first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:45:19 +03:00
commit 60db80892b
101 changed files with 16757 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,210 @@
package controllers
import (
"fmt"
database "goFiber/database/config"
"goFiber/database/models"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
"github.com/gofiber/fiber/v3"
)
// GetHero godoc
// @Summary Get active hero/banner
// @Tags Hero
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Router /api/v1/hero [get]
func GetHero(c fiber.Ctx) error {
var heroes []models.Hero
// Aktif olan tüm hero'ları getir
if err := database.DB.Where("is_active = ?", true).Find(&heroes).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
if len(heroes) == 0 {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "no active hero found"})
}
return c.JSON(heroes)
}
// GetHeroAll godoc
// @Summary Get all heroes
// @Description Returns all hero/banner records (no filter)
// @Tags Hero
// @Produce json
// @Success 200 {array} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Router /api/v1/heroes [get]
func GetHeroAll(c fiber.Ctx) error {
var heroes []models.Hero
// Tüm hero'ları getir (filtre yok)
if err := database.DB.Find(&heroes).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
if len(heroes) == 0 {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "no hero found"})
}
return c.JSON(heroes)
}
// CreateHero godoc
// @Summary Create new hero/banner (admin only)
// @Tags Hero
// @Accept mpfd
// @Produce json
// @Security BearerAuth
// @Param title formData string false "Title"
// @Param text1 formData string false "Text1"
// @Param text2 formData string false "Text2"
// @Param text4 formData string false "Text4"
// @Param text5 formData string false "Text5"
// @Param color formData string true "Color"
// @Param is_active formData boolean false "Is Active"
// @Param image formData file true "Hero Image"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Router /api/v1/hero [post]
func CreateHero(c fiber.Ctx) error {
var hero models.Hero
if err := c.Bind().Body(&hero); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
// Image upload
file, err := c.FormFile("image")
if err == nil {
if _, err := os.Stat("./uploads/heroes"); os.IsNotExist(err) {
os.MkdirAll("./uploads/heroes", 0755)
}
filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
filePath := filepath.Join("./uploads/heroes", filename)
if err := c.SaveFile(file, filePath); err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save image"})
}
hero.Image = "/uploads/heroes/" + filename
}
// Eğer sadece bir aktif hero olacaksa, diğerlerini pasife çekebiliriz
//if hero.IsActive {
// database.DB.Model(&models.Hero{}).Where("is_active = ?", true).Update("is_active", false)
//}
if err := database.DB.Create(&hero).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "hero could not be created"})
}
return c.Status(http.StatusCreated).JSON(hero)
}
// UpdateHero godoc
// @Summary Update hero/banner (admin only)
// @Tags Hero
// @Accept mpfd
// @Produce json
// @Security BearerAuth
// @Param id path int true "Hero ID"
// @Param title formData string false "Title"
// @Param text1 formData string false "Text1"
// @Param text2 formData string false "Text2"
// @Param text4 formData string false "Text4"
// @Param text5 formData string false "Text5"
// @Param color formData string false "Color"
// @Param is_active formData boolean false "Is Active"
// @Param image formData file false "Hero Image"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/hero/{id} [put]
func UpdateHero(c fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
var hero models.Hero
if err := database.DB.First(&hero, id).Error; err != nil {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "hero not found"})
}
// Log raw request body (works for JSON). For multipart/form-data, also log form values.
//log.Printf("Raw request body: %s\n", string(c.Body()))
//log.Printf("Form title: %s, is_active: %s\n", c.FormValue("title"), c.FormValue("is_active"))
var updateData models.Hero
if err := c.Bind().Body(&updateData); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
//log.Printf("Received update data: %+v\n", updateData) // Debug log
// Image upload
file, err := c.FormFile("image")
if err == nil {
if _, err := os.Stat("./uploads/heroes"); os.IsNotExist(err) {
os.MkdirAll("./uploads/heroes", 0755)
}
filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
filePath := filepath.Join("./uploads/heroes", filename)
if err := c.SaveFile(file, filePath); err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save image"})
}
updateData.Image = "/uploads/heroes/" + filename
}
// Eğer bu hero aktif yapılıyorsa diğerlerini pasife çek
//if updateData.IsActive {
// database.DB.Model(&models.Hero{}).Where("id != ?", id).Where("is_active = ?", true).Update("is_active", false)
//}
// Handle is_active coming from multipart/form-data: parse and update explicitly
if v := c.FormValue("is_active"); v != "" {
if parsed, err := strconv.ParseBool(v); err == nil {
// Ensure boolean field is updated even if it's false (zero value)
if err := database.DB.Model(&hero).Update("is_active", parsed).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "hero could not be updated"})
}
// reflect into updateData for consistency
updateData.IsActive = parsed
} else {
log.Printf("invalid is_active value: %s", v)
}
}
if err := database.DB.Model(&hero).Updates(updateData).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "hero could not be updated"})
}
return c.JSON(hero)
}
// DeleteHero godoc
// @Summary Delete hero/banner (admin only)
// @Tags Hero
// @Produce json
// @Security BearerAuth
// @Param id path int true "Hero ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/hero/{id} [delete]
func DeleteHero(c fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
var hero models.Hero
if err := database.DB.First(&hero, id).Error; err != nil {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "hero not found"})
}
if err := database.DB.Delete(&hero).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "hero could not be deleted"})
}
return c.JSON(fiber.Map{"message": "hero deleted successfully"})
}

View File

@@ -0,0 +1,565 @@
package controllers
import (
"encoding/json"
"errors"
configs "goFiber/config"
database "goFiber/database/config"
"goFiber/database/models"
"goFiber/middlewares"
"log"
"net/http"
"strconv"
"strings"
"github.com/gofiber/fiber/v3"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
const (
corsWhitelistCacheKey = "admin:cors:whitelist:list"
corsBlacklistCacheKey = "admin:cors:blacklist:list"
rateLimitCacheKey = "admin:rate_limit:list"
securityCacheTTL = 60
)
type CorsWhitelistRequest struct {
Origin string `json:"origin" validate:"required"`
Description string `json:"description"`
IsActive *bool `json:"is_active"`
}
type CorsBlacklistRequest struct {
Origin string `json:"origin" validate:"required"`
Reason string `json:"reason"`
IsActive *bool `json:"is_active"`
}
type RateLimitSettingRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description"`
MaxRequests int64 `json:"max_requests" validate:"required,min=1"`
WindowSeconds int `json:"window_seconds" validate:"required,min=1"`
IsActive *bool `json:"is_active"`
}
// ListCorsWhitelists godoc
// @Summary List CORS whitelists (admin only)
// @Tags Admin Security
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/cors/whitelist [get]
func ListCorsWhitelists(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
var items []models.CorsWhitelist
if cached, err := database.Get(corsWhitelistCacheKey); err == nil {
if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil {
securityLogf("[security][cors-whitelist][cache-hit] count=%d", len(items))
return c.JSON(fiber.Map{"count": len(items), "items": items})
}
} else if !errors.Is(err, redis.Nil) {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"})
}
if err := database.DB.Order("id DESC").Find(&items).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
cacheJSON, _ := json.Marshal(items)
_ = database.SetEx(corsWhitelistCacheKey, string(cacheJSON), securityCacheTTL)
securityLogf("[security][cors-whitelist][db-load] count=%d", len(items))
return c.JSON(fiber.Map{"count": len(items), "items": items})
}
// CreateCorsWhitelist godoc
// @Summary Create CORS whitelist (admin only)
// @Tags Admin Security
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body CorsWhitelistRequest true "Whitelist payload"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Router /api/v1/admin/cors/whitelist [post]
func CreateCorsWhitelist(c fiber.Ctx) error {
var req CorsWhitelistRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if err := validate.Struct(req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
item := models.CorsWhitelist{
Origin: strings.TrimSpace(req.Origin),
Description: strings.TrimSpace(req.Description),
IsActive: boolValue(req.IsActive, true),
CreatedBy: currentActor(c),
}
if err := database.DB.Create(&item).Error; err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"})
}
invalidateSecurityCaches()
securityLogf("[security][cors-whitelist][create] origin=%s by=%s", item.Origin, item.CreatedBy)
return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item})
}
// UpdateCorsWhitelist godoc
// @Summary Update CORS whitelist (admin only)
// @Tags Admin Security
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Whitelist ID"
// @Param request body CorsWhitelistRequest true "Whitelist payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/cors/whitelist/{id} [put]
func UpdateCorsWhitelist(c fiber.Ctx) error {
id, err := parseID(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
var req CorsWhitelistRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if err := validate.Struct(req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
var item models.CorsWhitelist
if err := database.DB.First(&item, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
item.Origin = strings.TrimSpace(req.Origin)
item.Description = strings.TrimSpace(req.Description)
item.IsActive = boolValue(req.IsActive, item.IsActive)
item.CreatedBy = currentActor(c)
if err := database.DB.Save(&item).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"})
}
invalidateSecurityCaches()
securityLogf("[security][cors-whitelist][update] id=%d origin=%s by=%s", item.ID, item.Origin, item.CreatedBy)
return c.JSON(fiber.Map{"item": item})
}
// DeleteCorsWhitelist godoc
// @Summary Soft delete CORS whitelist (admin only)
// @Tags Admin Security
// @Produce json
// @Security BearerAuth
// @Param id path int true "Whitelist ID"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/cors/whitelist/{id} [delete]
func DeleteCorsWhitelist(c fiber.Ctx) error {
id, err := parseID(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
if err := database.DB.Delete(&models.CorsWhitelist{}, id).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"})
}
invalidateSecurityCaches()
securityLogf("[security][cors-whitelist][soft-delete] id=%d by=%s", id, currentActor(c))
return c.JSON(fiber.Map{"message": "soft deleted", "id": id})
}
// HardDeleteCorsWhitelist godoc
// @Summary Hard delete CORS whitelist (admin only)
// @Tags Admin Security
// @Produce json
// @Security BearerAuth
// @Param id path int true "Whitelist ID"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/cors/whitelist/{id}/hard [delete]
func HardDeleteCorsWhitelist(c fiber.Ctx) error {
id, err := parseID(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
if err := database.DB.Unscoped().Delete(&models.CorsWhitelist{}, id).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"})
}
invalidateSecurityCaches()
securityLogf("[security][cors-whitelist][hard-delete] id=%d by=%s", id, currentActor(c))
return c.JSON(fiber.Map{"message": "hard deleted", "id": id})
}
// ListCorsBlacklists godoc
// @Summary List CORS blacklists (admin only)
// @Tags Admin Security
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/cors/blacklist [get]
func ListCorsBlacklists(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
var items []models.CorsBlacklist
if cached, err := database.Get(corsBlacklistCacheKey); err == nil {
if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil {
securityLogf("[security][cors-blacklist][cache-hit] count=%d", len(items))
return c.JSON(fiber.Map{"count": len(items), "items": items})
}
} else if !errors.Is(err, redis.Nil) {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"})
}
if err := database.DB.Order("id DESC").Find(&items).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
cacheJSON, _ := json.Marshal(items)
_ = database.SetEx(corsBlacklistCacheKey, string(cacheJSON), securityCacheTTL)
securityLogf("[security][cors-blacklist][db-load] count=%d", len(items))
return c.JSON(fiber.Map{"count": len(items), "items": items})
}
// CreateCorsBlacklist godoc
// @Summary Create CORS blacklist (admin only)
// @Tags Admin Security
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body CorsBlacklistRequest true "Blacklist payload"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Router /api/v1/admin/cors/blacklist [post]
func CreateCorsBlacklist(c fiber.Ctx) error {
var req CorsBlacklistRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if err := validate.Struct(req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
item := models.CorsBlacklist{
Origin: strings.TrimSpace(req.Origin),
Reason: strings.TrimSpace(req.Reason),
IsActive: boolValue(req.IsActive, true),
CreatedBy: currentActor(c),
}
if err := database.DB.Create(&item).Error; err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"})
}
invalidateSecurityCaches()
securityLogf("[security][cors-blacklist][create] origin=%s by=%s", item.Origin, item.CreatedBy)
return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item})
}
// UpdateCorsBlacklist godoc
// @Summary Update CORS blacklist (admin only)
// @Tags Admin Security
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Blacklist ID"
// @Param request body CorsBlacklistRequest true "Blacklist payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/cors/blacklist/{id} [put]
func UpdateCorsBlacklist(c fiber.Ctx) error {
id, err := parseID(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
var req CorsBlacklistRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if err := validate.Struct(req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
var item models.CorsBlacklist
if err := database.DB.First(&item, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
item.Origin = strings.TrimSpace(req.Origin)
item.Reason = strings.TrimSpace(req.Reason)
item.IsActive = boolValue(req.IsActive, item.IsActive)
item.CreatedBy = currentActor(c)
if err := database.DB.Save(&item).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"})
}
invalidateSecurityCaches()
securityLogf("[security][cors-blacklist][update] id=%d origin=%s by=%s", item.ID, item.Origin, item.CreatedBy)
return c.JSON(fiber.Map{"item": item})
}
// DeleteCorsBlacklist godoc
// @Summary Soft delete CORS blacklist (admin only)
// @Tags Admin Security
// @Produce json
// @Security BearerAuth
// @Param id path int true "Blacklist ID"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/cors/blacklist/{id} [delete]
func DeleteCorsBlacklist(c fiber.Ctx) error {
id, err := parseID(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
if err := database.DB.Delete(&models.CorsBlacklist{}, id).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"})
}
invalidateSecurityCaches()
securityLogf("[security][cors-blacklist][soft-delete] id=%d by=%s", id, currentActor(c))
return c.JSON(fiber.Map{"message": "soft deleted", "id": id})
}
// HardDeleteCorsBlacklist godoc
// @Summary Hard delete CORS blacklist (admin only)
// @Tags Admin Security
// @Produce json
// @Security BearerAuth
// @Param id path int true "Blacklist ID"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/cors/blacklist/{id}/hard [delete]
func HardDeleteCorsBlacklist(c fiber.Ctx) error {
id, err := parseID(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
if err := database.DB.Unscoped().Delete(&models.CorsBlacklist{}, id).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"})
}
invalidateSecurityCaches()
securityLogf("[security][cors-blacklist][hard-delete] id=%d by=%s", id, currentActor(c))
return c.JSON(fiber.Map{"message": "hard deleted", "id": id})
}
// ListRateLimitSettings godoc
// @Summary List rate limit settings (admin only)
// @Tags Admin Security
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/rate-limit [get]
func ListRateLimitSettings(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
var items []models.RateLimitSetting
if cached, err := database.Get(rateLimitCacheKey); err == nil {
if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil {
securityLogf("[security][rate-limit][cache-hit] count=%d", len(items))
return c.JSON(fiber.Map{"count": len(items), "items": items})
}
} else if !errors.Is(err, redis.Nil) {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"})
}
if err := database.DB.Order("id DESC").Find(&items).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
cacheJSON, _ := json.Marshal(items)
_ = database.SetEx(rateLimitCacheKey, string(cacheJSON), securityCacheTTL)
securityLogf("[security][rate-limit][db-load] count=%d", len(items))
return c.JSON(fiber.Map{"count": len(items), "items": items})
}
// CreateRateLimitSetting godoc
// @Summary Create rate limit setting (admin only)
// @Tags Admin Security
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body RateLimitSettingRequest true "Rate limit payload"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Router /api/v1/admin/rate-limit [post]
func CreateRateLimitSetting(c fiber.Ctx) error {
var req RateLimitSettingRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if err := validate.Struct(req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
item := models.RateLimitSetting{
Name: strings.TrimSpace(req.Name),
Description: strings.TrimSpace(req.Description),
MaxRequests: req.MaxRequests,
WindowSeconds: req.WindowSeconds,
IsActive: boolValue(req.IsActive, true),
UpdatedBy: currentActor(c),
}
if err := database.DB.Create(&item).Error; err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"})
}
invalidateSecurityCaches()
securityLogf("[security][rate-limit][create] name=%s max=%d window=%ds by=%s", item.Name, item.MaxRequests, item.WindowSeconds, item.UpdatedBy)
return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item})
}
// UpdateRateLimitSetting godoc
// @Summary Update rate limit setting (admin only)
// @Tags Admin Security
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Rate limit ID"
// @Param request body RateLimitSettingRequest true "Rate limit payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/rate-limit/{id} [put]
func UpdateRateLimitSetting(c fiber.Ctx) error {
id, err := parseID(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
var req RateLimitSettingRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if err := validate.Struct(req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
var item models.RateLimitSetting
if err := database.DB.First(&item, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
item.Name = strings.TrimSpace(req.Name)
item.Description = strings.TrimSpace(req.Description)
item.MaxRequests = req.MaxRequests
item.WindowSeconds = req.WindowSeconds
item.IsActive = boolValue(req.IsActive, item.IsActive)
item.UpdatedBy = currentActor(c)
if err := database.DB.Save(&item).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"})
}
invalidateSecurityCaches()
securityLogf("[security][rate-limit][update] id=%d name=%s max=%d window=%ds by=%s", item.ID, item.Name, item.MaxRequests, item.WindowSeconds, item.UpdatedBy)
return c.JSON(fiber.Map{"item": item})
}
// DeleteRateLimitSetting godoc
// @Summary Soft delete rate limit setting (admin only)
// @Tags Admin Security
// @Produce json
// @Security BearerAuth
// @Param id path int true "Rate limit ID"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/rate-limit/{id} [delete]
func DeleteRateLimitSetting(c fiber.Ctx) error {
id, err := parseID(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
if err := database.DB.Delete(&models.RateLimitSetting{}, id).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"})
}
invalidateSecurityCaches()
securityLogf("[security][rate-limit][soft-delete] id=%d by=%s", id, currentActor(c))
return c.JSON(fiber.Map{"message": "soft deleted", "id": id})
}
// HardDeleteRateLimitSetting godoc
// @Summary Hard delete rate limit setting (admin only)
// @Tags Admin Security
// @Produce json
// @Security BearerAuth
// @Param id path int true "Rate limit ID"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/rate-limit/{id}/hard [delete]
func HardDeleteRateLimitSetting(c fiber.Ctx) error {
id, err := parseID(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
if err := database.DB.Unscoped().Delete(&models.RateLimitSetting{}, id).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"})
}
invalidateSecurityCaches()
securityLogf("[security][rate-limit][hard-delete] id=%d by=%s", id, currentActor(c))
return c.JSON(fiber.Map{"message": "hard deleted", "id": id})
}
func parseID(param string) (uint, error) {
v, err := strconv.ParseUint(strings.TrimSpace(param), 10, 64)
if err != nil || v == 0 {
return 0, errors.New("invalid id")
}
return uint(v), nil
}
func invalidateSecurityCaches() {
_ = database.Delete(corsWhitelistCacheKey)
_ = database.Delete(corsBlacklistCacheKey)
_ = database.Delete(rateLimitCacheKey)
_ = database.Delete("cors:active:whitelist")
_ = database.Delete("cors:active:blacklist")
}
func currentActor(c fiber.Ctx) string {
if claims, ok := middlewares.GetAuthClaims(c); ok && strings.TrimSpace(claims.Email) != "" {
return claims.Email
}
return "system"
}
func boolValue(v *bool, fallback bool) bool {
if v == nil {
return fallback
}
return *v
}
func securityLogf(format string, args ...interface{}) {
if configs.AppConfig != nil && configs.AppConfig.CorsDebug {
log.Printf(format, args...)
}
}

View File

@@ -0,0 +1,218 @@
package controllers
import (
"fmt"
database "goFiber/database/config"
"goFiber/database/models"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
)
// GetSetting godoc
// @Summary Get site settings
// @Tags Setting
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Router /api/v1/setting [get]
func GetSetting(c fiber.Ctx) error {
var setting models.Setting
// Arkaplanda tek bir aktif ayar varsayıyoruz veya en son ekleneni/güncelleneni
if err := database.DB.Where("is_active = ?", true).Last(&setting).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "no active setting found"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
return c.JSON(setting)
}
// CreateSetting godoc
// @Summary Create new site setting (admin only)
// @Tags Setting
// @Accept mpfd
// @Produce json
// @Security BearerAuth
// @Param title formData string true "Title"
// @Param meta_title formData string true "Meta Title"
// @Param meta_description formData string true "Meta Description"
// @Param phone formData string true "Phone"
// @Param url formData string true "URL"
// @Param email formData string true "Email"
// @Param facebook formData string false "Facebook"
// @Param x formData string false "X"
// @Param instagram formData string false "Instagram"
// @Param whatsapp formData string false "Whatsapp"
// @Param pinterest formData string false "Pinterest"
// @Param linkedin formData string false "Linkedin"
// @Param slogan formData string false "Slogan"
// @Param address formData string false "Address"
// @Param copyright formData string false "Copyright"
// @Param map_embed formData string false "Map Embed"
// @Param is_active formData boolean false "Is Active"
// @Param w_logo formData file false "White Logo"
// @Param b_logo formData file false "Black Logo"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Router /api/v1/setting [post]
func CreateSetting(c fiber.Ctx) error {
var setting models.Setting
if err := c.Bind().Body(&setting); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
// White Logo upload
if file, err := c.FormFile("w_logo"); err == nil {
if _, err := os.Stat("./uploads/settings"); os.IsNotExist(err) {
os.MkdirAll("./uploads/settings", 0755)
}
filename := fmt.Sprintf("w_%d_%s", time.Now().Unix(), file.Filename)
filePath := filepath.Join("./uploads/settings", filename)
if err := c.SaveFile(file, filePath); err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save w_logo"})
}
setting.WLogo = "/uploads/settings/" + filename
}
// Black Logo upload
if file, err := c.FormFile("b_logo"); err == nil {
if _, err := os.Stat("./uploads/settings"); os.IsNotExist(err) {
os.MkdirAll("./uploads/settings", 0755)
}
filename := fmt.Sprintf("b_%d_%s", time.Now().Unix(), file.Filename)
filePath := filepath.Join("./uploads/settings", filename)
if err := c.SaveFile(file, filePath); err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save b_logo"})
}
setting.BLogo = "/uploads/settings/" + filename
}
// Eğer sadece bir aktif ayar olacaksa, diğerlerini pasife çekebiliriz
if setting.IsActive {
database.DB.Model(&models.Setting{}).Where("is_active = ?", true).Update("is_active", false)
}
if err := database.DB.Create(&setting).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "setting could not be created"})
}
return c.Status(http.StatusCreated).JSON(setting)
}
// UpdateSetting godoc
// @Summary Update site setting (admin only)
// @Tags Setting
// @Accept mpfd
// @Produce json
// @Security BearerAuth
// @Param id path int true "Setting ID"
// @Param title formData string false "Title"
// @Param meta_title formData string false "Meta Title"
// @Param meta_description formData string false "Meta Description"
// @Param phone formData string false "Phone"
// @Param url formData string false "URL"
// @Param email formData string false "Email"
// @Param facebook formData string false "Facebook"
// @Param x formData string false "X"
// @Param instagram formData string false "Instagram"
// @Param whatsapp formData string false "Whatsapp"
// @Param pinterest formData string false "Pinterest"
// @Param linkedin formData string false "Linkedin"
// @Param slogan formData string false "Slogan"
// @Param address formData string false "Address"
// @Param copyright formData string false "Copyright"
// @Param map_embed formData string false "Map Embed"
// @Param is_active formData boolean false "Is Active"
// @Param w_logo formData file false "White Logo"
// @Param b_logo formData file false "Black Logo"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/setting/{id} [put]
func UpdateSetting(c fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
var setting models.Setting
if err := database.DB.First(&setting, id).Error; err != nil {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "setting not found"})
}
var updateData models.Setting
if err := c.Bind().Body(&updateData); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
// White Logo upload
if file, err := c.FormFile("w_logo"); err == nil {
if _, err := os.Stat("./uploads/settings"); os.IsNotExist(err) {
os.MkdirAll("./uploads/settings", 0755)
}
filename := fmt.Sprintf("w_%d_%s", time.Now().Unix(), file.Filename)
filePath := filepath.Join("./uploads/settings", filename)
if err := c.SaveFile(file, filePath); err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save w_logo"})
}
updateData.WLogo = "/uploads/settings/" + filename
}
// Black Logo upload
if file, err := c.FormFile("b_logo"); err == nil {
if _, err := os.Stat("./uploads/settings"); os.IsNotExist(err) {
os.MkdirAll("./uploads/settings", 0755)
}
filename := fmt.Sprintf("b_%d_%s", time.Now().Unix(), file.Filename)
filePath := filepath.Join("./uploads/settings", filename)
if err := c.SaveFile(file, filePath); err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save b_logo"})
}
updateData.BLogo = "/uploads/settings/" + filename
}
// Eğer bu ayar aktif yapılıyorsa diğerlerini pasife çek
if updateData.IsActive {
database.DB.Model(&models.Setting{}).Where("id != ?", id).Where("is_active = ?", true).Update("is_active", false)
}
if err := database.DB.Model(&setting).Updates(updateData).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "setting could not be updated"})
}
return c.JSON(setting)
}
// DeleteSetting godoc
// @Summary Delete site setting (admin only)
// @Tags Setting
// @Produce json
// @Security BearerAuth
// @Param id path int true "Setting ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/setting/{id} [delete]
func DeleteSetting(c fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
var setting models.Setting
if err := database.DB.First(&setting, id).Error; err != nil {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "setting not found"})
}
if err := database.DB.Delete(&setting).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "setting could not be deleted"})
}
return c.JSON(fiber.Map{"message": "setting deleted successfully"})
}

890
controllers/user.go Normal file
View File

@@ -0,0 +1,890 @@
package controllers
import (
"encoding/json"
"errors"
"fmt"
configs "goFiber/config"
database "goFiber/database/config"
"goFiber/database/models"
"goFiber/middlewares"
utils "goFiber/pkg/utis"
"goFiber/services"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v3"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
var validate = validator.New()
type RegisterRequest struct {
UserName string `json:"username" validate:"required,min=3"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=6"`
FirstName string `json:"first_name" validate:"required"`
LastName string `json:"last_name" validate:"required"`
}
type LoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
type RefreshRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
type ResendVerificationRequest struct {
Email string `json:"email" validate:"required,email"`
}
// UpdateUserRequest represents allowed fields for updating a user
type UpdateUserRequest struct {
UserName string `json:"username,omitempty" example:"jdoe"`
Email string `json:"email,omitempty" example:"jdoe@example.com"`
IsAdmin *bool `json:"is_admin,omitempty" example:"false"`
Password string `json:"password,omitempty" example:"#secret"`
FirstName string `json:"first_name,omitempty" example:"John"`
LastName string `json:"last_name,omitempty" example:"Doe"`
AvatarURL string `json:"avatar_url,omitempty" example:"/uploads/avatar.jpg"`
EmailVerified *bool `json:"email_verified,omitempty" example:"true"`
// Accept avatar file via multipart/form-data with field name "avatar" when using form upload
}
func GetUser(c fiber.Ctx) error {
return c.Status(fiber.StatusOK).SendString("Get User")
}
// AdminListUsers godoc
// @Summary List active users (admin only)
// @Tags Users
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/users/list [get]
func AdminListUsers(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
var users []models.User
if err := database.DB.Preload("Profile").Order("id DESC").Find(&users).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
return c.JSON(fiber.Map{
"count": len(users),
"users": users,
})
}
// AdminListDeletedUsers godoc
// @Summary List soft-deleted users (admin only)
// @Tags Users
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/users/list/deleted [get]
func AdminListDeletedUsers(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
var users []models.User
if err := database.DB.Unscoped().
Preload("Profile").
Where("deleted_at IS NOT NULL").
Order("deleted_at DESC").
Find(&users).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
return c.JSON(fiber.Map{
"count": len(users),
"users": users,
})
}
func GetUserOne(c fiber.Ctx) error {
return c.Status(fiber.StatusOK).SendString("Get User One")
}
// UpdateUser godoc
// @Summary Update user (admin only)
// @Tags Users
// @Accept mpfd
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Param username formData string false "Username"
// @Param email formData string false "Email"
// @Param is_admin formData boolean false "Is Admin"
// @Param password formData string false "Password"
// @Param first_name formData string false "First Name"
// @Param last_name formData string false "Last Name"
// @Param email_verified formData boolean false "Email Verified"
// @Param avatar formData file false "Avatar Image"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/users/{id} [put]
func UpdateUser(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil || id == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
}
var user models.User
if err := database.DB.Preload("Profile").First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
// Parse incoming JSON or multipart/form-data into map to allow partial updates including false values
var payload map[string]interface{}
// Prefer detecting multipart by trying to read the multipart form first
if mf, err := c.MultipartForm(); err == nil && mf != nil {
payload = map[string]interface{}{}
// form values
for k, vals := range mf.Value {
if len(vals) > 0 {
payload[k] = vals[0]
}
}
// handle avatar file if present
if files, ok := mf.File["avatar"]; ok && len(files) > 0 {
file := files[0]
if _, err := os.Stat("./uploads/avatars"); os.IsNotExist(err) {
os.MkdirAll("./uploads/avatars", 0755)
}
filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
filePath := filepath.Join("./uploads/avatars", filename)
if err := c.SaveFile(file, filePath); err == nil {
payload["avatar_url"] = "/uploads/avatars/" + filename
} else {
log.Printf("failed to save avatar: %v", err)
}
}
} else {
// fallback to JSON body
if err := json.Unmarshal(c.Body(), &payload); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
}
// Prepare updates for user table
userUpdates := map[string]interface{}{}
if v, ok := payload["username"].(string); ok {
userUpdates["user_name"] = v
userUpdates["user_name"] = v
user.UserName = v
}
if v, ok := payload["email"].(string); ok {
userUpdates["email"] = v
user.Email = v
}
if v, ok := payload["is_admin"]; ok {
// handle bool or string representations
switch val := v.(type) {
case bool:
userUpdates["is_admin"] = val
user.IsAdmin = &val
case string:
if parsed, err := strconv.ParseBool(val); err == nil {
userUpdates["is_admin"] = parsed
user.IsAdmin = &parsed
}
}
}
// Handle email_verified explicitly (bool or string)
if v, ok := payload["email_verified"]; ok {
switch val := v.(type) {
case bool:
userUpdates["email_verified"] = val
now := time.Now()
if val {
userUpdates["email_verified_at"] = now
user.EmailVerified = &val
user.EmailVerifiedAt = &now
} else {
userUpdates["email_verified_at"] = nil
user.EmailVerified = &val
user.EmailVerifiedAt = nil
}
case string:
if parsed, err := strconv.ParseBool(val); err == nil {
userUpdates["email_verified"] = parsed
now := time.Now()
if parsed {
userUpdates["email_verified_at"] = now
user.EmailVerified = &parsed
user.EmailVerifiedAt = &now
} else {
userUpdates["email_verified_at"] = nil
user.EmailVerified = &parsed
user.EmailVerifiedAt = nil
}
}
}
}
if v, ok := payload["password"].(string); ok && v != "" {
hashed, err := bcrypt.GenerateFromPassword([]byte(v), bcrypt.DefaultCost)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not hash password"})
}
userUpdates["password"] = string(hashed)
user.Password = string(hashed)
}
// Apply user updates if any
if len(userUpdates) > 0 {
if err := database.DB.Model(&user).Updates(userUpdates).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be updated"})
}
}
// Handle profile updates (first_name, last_name, avatar_url)
profileUpdates := map[string]interface{}{}
if v, ok := payload["first_name"].(string); ok {
profileUpdates["first_name"] = v
}
if v, ok := payload["last_name"].(string); ok {
profileUpdates["last_name"] = v
}
if v, ok := payload["avatar_url"].(string); ok {
profileUpdates["avatar_url"] = v
}
if len(profileUpdates) > 0 {
// Profile may be stored as slice; update first profile if exists else create
var profile models.Profile
if len(user.Profile) > 0 {
profile = user.Profile[0]
if err := database.DB.Model(&profile).Updates(profileUpdates).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be updated"})
}
} else {
profile = models.Profile{
UserID: uint64(user.ID),
}
if v, ok := profileUpdates["first_name"].(string); ok {
profile.FirstName = v
}
if v, ok := profileUpdates["last_name"].(string); ok {
profile.LastName = v
}
if v, ok := profileUpdates["avatar_url"].(string); ok {
profile.AvatarURL = v
}
if err := database.DB.Create(&profile).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be created"})
}
}
}
// Reload user with profile
if err := database.DB.Preload("Profile").First(&user, id).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
return c.JSON(fiber.Map{"message": "user updated", "user": user})
}
// DeleteUser godoc
// @Summary Soft delete user (admin only)
// @Tags Users
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/users/{id} [delete]
func DeleteUser(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil || id == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
}
var user models.User
if err := database.DB.First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
if err := database.DB.Delete(&user).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be deleted"})
}
return c.JSON(fiber.Map{
"message": "user soft-deleted successfully",
"user_id": id,
})
}
// HardDeleteUser godoc
// @Summary Hard delete user permanently (admin only)
// @Tags Users
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/users/{id}/hard [delete]
func HardDeleteUser(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil || id == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
}
var user models.User
if err := database.DB.Unscoped().First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
err = database.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Where("user_id = ?", id).Delete(&models.Profile{}).Error; err != nil {
return err
}
if err := tx.Unscoped().Where("user_id = ?", id).Delete(&models.SocialAccount{}).Error; err != nil {
return err
}
return tx.Unscoped().Delete(&models.User{}, id).Error
})
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user hard-delete failed"})
}
return c.JSON(fiber.Map{
"message": "user permanently deleted",
"user_id": id,
})
}
// RestoreUser godoc
// @Summary Restore soft-deleted user (admin only)
// @Tags Users
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/users/{id}/restore [post]
func RestoreUser(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil || id == 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
}
var user models.User
if err := database.DB.Unscoped().First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
if !user.DeletedAt.Valid {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "user is not soft-deleted"})
}
if err := database.DB.Unscoped().Model(&models.User{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be restored"})
}
return c.JSON(fiber.Map{
"message": "user restored successfully",
"user_id": id,
})
}
// Register godoc
// @Summary Register user
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body RegisterRequest true "Register payload"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 409 {object} map[string]string
// @Router /api/v1/auth/register [post]
func Register(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
var req RegisterRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if err := validate.Struct(req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "password could not be hashed"})
}
verifyToken, err := utils.GenerateSecureToken(32)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verify token could not be generated"})
}
user := models.User{
UserName: req.UserName,
Email: req.Email,
Password: string(hashedPassword),
EmailVerifyToken: verifyToken,
}
if err := database.DB.Create(&user).Error; err != nil {
return c.Status(http.StatusConflict).JSON(fiber.Map{"error": "email already exists"})
}
profile := models.Profile{
UserID: uint64(user.ID),
FirstName: req.FirstName,
LastName: req.LastName,
}
if err := database.DB.Create(&profile).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be created"})
}
appURL := strings.TrimRight(configs.AppConfig.AppURL, "/")
verifyURL := fmt.Sprintf("%s/api/v1/auth/verify-email?token=%s", appURL, url.QueryEscape(verifyToken))
emailService := services.NewEmailService()
err = emailService.SendVerificationEmail(user.Email, profile.FirstName, verifyURL)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification email could not be sent"})
}
return c.Status(http.StatusCreated).JSON(fiber.Map{
"message": "registration successful, please verify your email before login",
"user": fiber.Map{
"id": user.ID,
"username": user.UserName,
"email": user.Email,
"is_admin": boolPtrValue(user.IsAdmin),
"email_verified": false,
"first_name": profile.FirstName,
"last_name": profile.LastName,
},
})
}
// Login godoc
// @Summary Login user
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body LoginRequest true "Login payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/login [post]
func Login(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
var req LoginRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if err := validate.Struct(req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
var user models.User
if err := database.DB.Preload("Profile").Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid email or password"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid email or password"})
}
if !user.IsEmailVerified() {
return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": "please verify your email before login"})
}
firstName, lastName := extractProfileName(user.Profile)
jwtService := services.NewJWTService()
accessToken, refreshToken, err := jwtService.GenerateTokenPair(
user.ID,
user.Email,
boolPtrValue(user.IsAdmin),
firstName,
lastName,
)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "tokens could not be generated"})
}
return c.JSON(fiber.Map{
"user": fiber.Map{
"id": user.ID,
"username": user.UserName,
"email": user.Email,
"is_admin": boolPtrValue(user.IsAdmin),
"first_name": firstName,
"last_name": lastName,
},
"access_token": accessToken,
"refresh_token": refreshToken,
})
}
// RefreshToken godoc
// @Summary Refresh access token
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body RefreshRequest true "Refresh payload"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/refresh [post]
func RefreshToken(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
var req RefreshRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if err := validate.Struct(req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
jwtService := services.NewJWTService()
claims, err := jwtService.ValidateToken(req.RefreshToken)
if err != nil || claims.TokenType != services.TokenTypeRefresh {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid refresh token"})
}
var user models.User
if err := database.DB.Preload("Profile").First(&user, claims.UserID).Error; err != nil {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "user not found"})
}
firstName, lastName := extractProfileName(user.Profile)
accessToken, refreshToken, err := jwtService.GenerateTokenPair(
user.ID,
user.Email,
boolPtrValue(user.IsAdmin),
firstName,
lastName,
)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "tokens could not be generated"})
}
fmt.Println(accessToken, "Access Token Yenilendi !!!")
return c.JSON(fiber.Map{
"access_token": accessToken,
"refresh_token": refreshToken,
})
}
// VerifyEmail godoc
// @Summary Verify email address with token
// @Tags Auth
// @Produce json
// @Param token query string true "Email verify token"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/auth/verify-email [get]
func VerifyEmail(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
token := strings.TrimSpace(c.Query("token"))
if token == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "token is required"})
}
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 c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "invalid or expired token"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
now := time.Now()
isVerified := true
user.EmailVerified = &isVerified
user.EmailVerifiedAt = &now
user.EmailVerifyToken = ""
if err := database.DB.Save(&user).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "email verification could not be saved"})
}
return c.JSON(fiber.Map{"message": "email verified successfully"})
}
// ResendVerificationEmail godoc
// @Summary Resend verification email
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body ResendVerificationRequest true "Resend verification payload"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/auth/resend-verification [post]
func ResendVerificationEmail(c fiber.Ctx) error {
if database.DB == nil {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
}
var req ResendVerificationRequest
if err := c.Bind().JSON(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
if err := validate.Struct(req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
var user models.User
if err := database.DB.Preload("Profile").Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
if user.IsEmailVerified() {
return c.JSON(fiber.Map{"message": "email is already verified"})
}
verifyToken, err := utils.GenerateSecureToken(32)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verify token could not be generated"})
}
user.EmailVerifyToken = verifyToken
if err := database.DB.Save(&user).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification token could not be saved"})
}
firstName, _ := extractProfileName(user.Profile)
appURL := strings.TrimRight(configs.AppConfig.AppURL, "/")
verifyURL := fmt.Sprintf("%s/api/v1/auth/verify-email?token=%s", appURL, url.QueryEscape(verifyToken))
emailService := services.NewEmailService()
if err := emailService.SendVerificationEmail(user.Email, firstName, verifyURL); err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification email could not be sent"})
}
return c.JSON(fiber.Map{"message": "verification email has been sent"})
}
// Me godoc
// @Summary Get current user from token
// @Tags Auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Router /api/v1/auth/me [get]
func Me(c fiber.Ctx) error {
claims, ok := middlewares.GetAuthClaims(c)
if !ok {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
return c.JSON(fiber.Map{
"user": fiber.Map{
"id": claims.UserID,
"email": claims.Email,
"is_admin": claims.IsAdmin,
"first_name": claims.FirstName,
"last_name": claims.LastName,
},
})
}
// AdminOnlyExample godoc
// @Summary Admin-only sample endpoint
// @Tags Auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/auth/admin/example [get]
func AdminOnlyExample(c fiber.Ctx) error {
claims, ok := middlewares.GetAuthClaims(c)
if !ok {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
return c.JSON(fiber.Map{
"message": "only admins can access this endpoint",
"user": claims.Email,
})
}
// UserOnlyExample godoc
// @Summary Normal-user-only sample endpoint
// @Tags Auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/auth/user/example [get]
func UserOnlyExample(c fiber.Ctx) error {
claims, ok := middlewares.GetAuthClaims(c)
if !ok {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
return c.JSON(fiber.Map{
"message": "only normal users can access this endpoint",
"user": claims.Email,
})
}
func GoogleAuth(c fiber.Ctx) error {
if configs.AppConfig.GoogleClientID == "" {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "google oauth is not configured"})
}
stateToken, err := utils.GenerateSecureToken(16)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "state token could not be generated"})
}
authURL := "https://accounts.google.com/o/oauth2/v2/auth?" + url.Values{
"client_id": []string{configs.AppConfig.GoogleClientID},
"redirect_uri": []string{configs.AppConfig.GoogleRedirectURL},
"response_type": []string{"code"},
"scope": []string{"openid email profile"},
"state": []string{stateToken},
}.Encode()
return c.JSON(fiber.Map{"provider": "google", "auth_url": authURL, "state": stateToken})
}
func GoogleAuthCallback(c fiber.Ctx) error {
code := c.Query("code")
if code == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "google callback code is missing"})
}
// OAuth token exchange is intentionally left simple for now.
return c.JSON(fiber.Map{
"provider": "google",
"message": "google callback infrastructure is ready, token exchange can be added next",
"code": code,
"state": c.Query("state"),
})
}
func GithubAuth(c fiber.Ctx) error {
if configs.AppConfig.GithubClientID == "" {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "github oauth is not configured"})
}
stateToken, err := utils.GenerateSecureToken(16)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "state token could not be generated"})
}
authURL := "https://github.com/login/oauth/authorize?" + url.Values{
"client_id": []string{configs.AppConfig.GithubClientID},
"redirect_uri": []string{configs.AppConfig.GithubRedirectURL},
"scope": []string{"read:user user:email"},
"state": []string{stateToken},
}.Encode()
return c.JSON(fiber.Map{"provider": "github", "auth_url": authURL, "state": stateToken})
}
func GithubAuthCallback(c fiber.Ctx) error {
code := c.Query("code")
if code == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "github callback code is missing"})
}
// OAuth token exchange is intentionally left simple for now.
return c.JSON(fiber.Map{
"provider": "github",
"message": "github callback infrastructure is ready, token exchange can be added next",
"code": code,
"state": c.Query("state"),
})
}
func extractProfileName(profiles []models.Profile) (string, string) {
if len(profiles) == 0 {
return "", ""
}
return profiles[0].FirstName, profiles[0].LastName
}
func boolPtrValue(v *bool) bool {
if v == nil {
return false
}
return *v
}