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

58
.air.toml Normal file
View File

@@ -0,0 +1,58 @@
#:schema https://json.schemastore.org/any.json
env_files = []
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
entrypoint = ["./tmp/main"]
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
ignore_dangerous_root_dir = false
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
app_start_timeout = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
.git
.idea
.vscode
tmp
uploads
node_modules
dist
.DS_Store
main
.env
docker-compose.yml
Dockerfile

65
.env Normal file
View File

@@ -0,0 +1,65 @@
### Db Configuration
DB_URL="gofiber:gg7678290@tcp(10.80.80.70:3306)/gofiber?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s&readTimeout=30s&writeTimeout=30s&multiStatements=true"
##########################
# Redis Configuration
REDIS_HOST=10.80.80.70
REDIS_PORT=6379
REDIS_USER=default
REDIS_PASSWORD=gg7678290
REDIS_URL=redis://default:gg7678290@10.80.80.70:6379/4
#############################
# JWT Secret
JWT_SECRET=go-fibere-CT286MautyK9TWfz8SPSIWJTXV83mwLRwriLvSbpcMZuDLxywaHWP9Ju7xRoTS2
#############################
# Email Settings (Mailpit)
EMAIL_HOST=10.80.80.70
EMAIL_PORT=1025
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_USE_TLS=false
EMAIL_USE_SSL=false
EMAIL_FROM=noreply@gauth.local
#############################
# App Genel Ayarları
PORT=8080
################################
# AVATANE IMAGES
AVATAR_H=150
AVATAR_W=150
AVATAR_Q=90
AVATAR_B=cover
AVATAR_F=webp
#######################
# Home IMAGES
HOME_IMAGE_H=400
HOME_IMAGE_W=400
HOME_IMAGE_Q=90
HOME_IMAGE_B=cover
HOME_IMAGE_F=webp
#######################
# Aboutme IMAGES
ABOUTME_IMAGE_H=400
ABOUTME_IMAGE_W=400
ABOUTME_IMAGE_Q=90
ABOUTME_IMAGE_B=cover
ABOUTME_IMAGE_F=webp
#######################
# MyService IMAGES
SERVICE_IMAGE_H=256
SERVICE_IMAGE_W=256
SERVICE_IMAGE_Q=90
SERVICE_IMAGE_B=cover
SERVICE_IMAGE_F=webp
#######################
# BANNER IMAGES
BANNER_IMAGE_H=700
BANNER_IMAGE_W=1920
BANNER_IMAGE_Q=85
BANNER_IMAGE_B=cover
BANNER_IMAGE_F=webp
################################
################################
CORS_DEBUG=true
VITE_API_BASE_URL=http://localhost:8080
FRONTEND_URL=http://localhost:3000
SESSION_SECRET=go-fiber-mTFY2jAOMWWxadVIWjRoPG9aOM3z9srCVoU35Gs1VZaRKgXet26cztUE8LLpwok9

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
### Go template
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
tmp/
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
tmp
# Go workspace file
go.work
go.work.sum

61
Dockerfile Normal file
View File

@@ -0,0 +1,61 @@
# Build stage
FROM golang:1.25.7-alpine AS builder
WORKDIR /app
# Build-time args (passed from docker-compose build)
ARG DB_URL
ARG REDIS_URL
ARG REDIS_HOST
ARG REDIS_PORT
ARG EMAIL_HOST
ARG EMAIL_PORT
# Install build dependencies
RUN apk add --no-cache git
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download all dependencies
RUN go mod download
# Copy the source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# Final stage
FROM alpine:latest
WORKDIR /app
# Install necessary runtime dependencies
RUN apk add --no-cache ca-certificates tzdata
# Set runtime ENV from build args (1:1 usage)
ENV DB_URL=${DB_URL}
ENV REDIS_URL=${REDIS_URL}
ENV REDIS_HOST=${REDIS_HOST}
ENV REDIS_PORT=${REDIS_PORT}
ENV EMAIL_HOST=${EMAIL_HOST}
ENV EMAIL_PORT=${EMAIL_PORT}
# Copy the binary from builder
COPY --from=builder /app/main .
# Copy docs and views for static serving if not mounted
COPY docs ./docs
COPY views ./views
# Create uploads directory
RUN mkdir -p uploads
# Expose the port
EXPOSE 8080
# Command to run the executable
CMD ["./main"]

27
Prpmpt.md Normal file
View File

@@ -0,0 +1,27 @@
github.com/go-playground/validator/v10 v10.30.1
github.com/go-sql-driver/mysql v1.9.3
github.com/gofiber/fiber/v3 v3.0.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.48.0
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1
projedikde kullanılacak paketler bunlar şuan
paketlerinin versiyonlarının olduğu go.mod dosyasında görünüyor.
başka bir paket eklenmesi gerekirse go.mod dosyasına eklenmeli.
paketlerin versiyonlarini kesinlik ile değiştirmak yok !!
Uygulamada Yapmak istegim User için bir register ve login işlemi yapmak istiyorum.
Backend api hizmeti verek jwt token access_tokne ve refresh_token olacak.
access_token 120 dakika süre ile refresh_token 30 gün süre ile geçerli olacak.
access_token ve refresh_token için jwt token oluşturulacak.
access_token ve refresh_token için jwt token oluşturulurken user id ve email bilgileri is_admin bilgisi
Profile modelinin içindeki FirstName,LastName kullanılacak.
Github ve Google login register için gereken alt yapi ve endpoint apileri olusturulacak.
bunlari yarken benim kums oldugum klasor yapisi kullanilacak.
ve mumkun olduğuca her işlem basit anlaşilir tutulacak.

71
client.rest Normal file
View File

@@ -0,0 +1,71 @@
### Get all heroes (no auth)
GET http://localhost:8080/api/v1/heroes
Accept: application/json
### Get active heroes (no auth)
GET http://localhost:8080/api/v1/hero
Accept: application/json
### Update hero (JSON) — requires admin token
PUT http://localhost:8080/api/v1/hero/1
Content-Type: application/json
Authorization: Bearer {{ADMIN_TOKEN}}
{
"title": "updated-via-rest",
"is_active": false
}
### Update hero (multipart/form-data) — send file + is_active=false
PUT http://localhost:8080/api/v1/hero/1
Authorization: Bearer {{ADMIN_TOKEN}}
Content-Type: multipart/form-data; boundary=---011000010111000001101001
Content-Disposition: form-data; name="title"
multipart-update
Content-Disposition: form-data; name="is_active"
false
Content-Disposition: form-data; name="image"; filename="test.jpg"
Content-Type: image/jpeg
< ./path/to/test.jpg
### Delete hero (admin)
DELETE http://localhost:8080/api/v1/hero/1
Authorization: Bearer {{ADMIN_TOKEN}}
# Equivalent curl examples:
#
# curl GET all heroes
# curl -sS http://localhost:8080/api/v1/heroes | jq '.'
#
# curl update JSON
# curl -X PUT "http://localhost:8080/api/v1/hero/1" -H "Authorization: Bearer <ADMIN_TOKEN>" -H "Content-Type: application/json" -d '{"title":"test","is_active":false}'
#
# curl multipart (with image)
# curl -X PUT "http://localhost:8080/api/v1/hero/1" -H "Authorization: Bearer <ADMIN_TOKEN>" -F "title=multipart-test" -F "is_active=false" -F "image=@/absolute/path/to/image.jpg"
### Update user (multipart/form-data) — upload avatar + fields (admin)
PUT http://localhost:8080/api/v1/users/2
Authorization: Bearer {{ADMIN_TOKEN}}
Content-Type: multipart/form-data; boundary=---011000010111000001101001
-----011000010111000001101001
Content-Disposition: form-data; name="first_name"
Ayse
-----011000010111000001101001
Content-Disposition: form-data; name="email_verified"
false
-----011000010111000001101001
Content-Disposition: form-data; name="avatar"; filename="avatar.jpg"
Content-Type: image/jpeg
< ./path/to/avatar.jpg
-----011000010111000001101001--
# curl equivalent:
# curl -X PUT "http://localhost:8080/api/v1/users/2" -H "Authorization: Bearer <ADMIN_TOKEN>" -F "first_name=Ayse" -F "email_verified=false" -F "avatar=@/absolute/path/to/avatar.jpg"

235
config/config.go Normal file
View File

@@ -0,0 +1,235 @@
package configs
import (
"log"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
)
type Config struct {
Env string // örn. development, production
Port string
DBUrl string
JWTSecret string
AppURL string // örn. https://api.example.com - e-posta doğrulama linkleri için kullanılır
GoogleClientID string
GoogleClientSecret string
GithubClientID string
GithubClientSecret string
GoogleRedirectURL string
GithubRedirectURL string
ClientCallbackURL string
OAuthRedirectURL string
RedisUrl string
AccessTokenExpireMinutes int
RefreshTokenExpireDays int
// Avatar Ayarları
AvatarHeight int
AvatarWidth int
AvatarQuality int
AvatarFormat string
AvatarMode string // cover, contain, resize
// Ana Sayfa Resim Ayarları
HomeImageHeight int
HomeImageWidth int
HomeImageQuality int
HomeImageFormat string
HomeImageMode string // cover, contain, resize
// Hakkında Resim Ayarları
AboutImageHeight int
AboutImageWidth int
AboutImageQuality int
AboutImageFormat string
AboutImageMode string // cover, contain, resize
// Servis Resim Ayarları
ServiceImageHeight int
ServiceImageWidth int
ServiceImageQuality int
ServiceImageFormat string
ServiceImageMode string // cover, contain, resize
// Gönderi Resim Ayarları
PostImageHeight int
PostImageWidth int
PostImageQuality int
PostImageFormat string
PostImageMode string // cover, contain, resize
// Gönderi Kategori Resim Ayarları
PostCategoryImageHeight int
PostCategoryImageWidth int
PostCategoryImageQuality int
PostCategoryImageFormat string
PostCategoryImageMode string // cover, contain, resize
// Site Logo Ayarları
SettingsLogoHeight int
SettingsLogoWidth int
SettingsLogoQuality int
SettingsLogoFormat string
SettingsLogoMode string // cover, contain, resize
// Afiş Resim Ayarları
BannerImageHeight int
BannerImageWidth int
BannerImageQuality int
BannerImageFormat string
BannerImageMode string // cover, contain, resize
// Afiş Küçük Resim (Thumb) Ayarları
BannerThumbHeight int
BannerThumbWidth int
BannerThumbQuality int
BannerThumbFormat string
BannerThumbMode string // cover, contain, resize
// E-posta Ayarları
EmailHost string
EmailPort string
EmailHostUser string
EmailHostPassword string
EmailFrom string
CorsDebug bool
}
var AppConfig *Config
func LoadConfig() {
err := godotenv.Load()
if err != nil {
// Eğer proje kök dizininden çalıştırılmıyorsa (örn: cmd/app içinden), üst dizinleri kontrol et
err = godotenv.Load(".env")
//err = godotenv.Load("../../.env")
}
if err != nil {
log.Printf("Uyarı: .env dosyası yüklenirken hata oluştu: %v — sistem ortam değişkenleriyle devam ediliyor", err)
}
AppConfig = &Config{
Env: getEnv("APP_ENV", "development"),
Port: getEnv("PORT", "8080"),
DBUrl: getEnv("DB_URL", ""),
JWTSecret: getEnv("JWT_SECRET", "default_secret"),
AppURL: getEnv("APP_URL", "http://localhost:8080"),
GoogleClientID: getEnv("GOOGLE_CLIENT_ID", ""),
GoogleClientSecret: getEnv("GOOGLE_CLIENT_SECRET", ""),
GithubClientID: getEnv("GITHUB_CLIENT_ID", ""),
GithubClientSecret: getEnv("GITHUB_CLIENT_SECRET", ""),
GoogleRedirectURL: getEnv("GOOGLE_REDIRECT_URL", "http://localhost:8080/api/v1/auth/google/callback"),
GithubRedirectURL: getEnv("GITHUB_REDIRECT_URL", "http://localhost:8080/api/v1/auth/github/callback"),
ClientCallbackURL: getEnv("CLIENT_CALLBACK_URL", ""),
OAuthRedirectURL: getEnv("OAUTH_REDIRECT_URL", ""),
RedisUrl: getEnv("REDIS_URL", ""),
AccessTokenExpireMinutes: getEnvAsInt("ACCESS_TOKEN_EXPIRE_MINUTES", 120), // Varsayılan 120 dakika
RefreshTokenExpireDays: getEnvAsInt("REFRESH_TOKEN_EXPIRE_DAYS", 30), // Varsayılan 30 gün
// Avatar Varsayılanları
AvatarHeight: getEnvAsInt("AVATAR_H", 0), // Varsayılan 0 (otomatik)
AvatarWidth: getEnvAsInt("AVATAR_W", 800), // Varsayılan 800
AvatarQuality: getEnvAsInt("AVATAR_Q", 80), // Varsayılan 80
AvatarFormat: getEnv("AVATAR_F", "webp"), // Varsayılan webp
AvatarMode: getEnv("AVATAR_B", "contain"), // Varsayılan contain (Fit)
// Ana Sayfa Resim Varsayılanları
HomeImageHeight: getEnvAsInt("HOME_IMAGE_H", 0), // Varsayılan 0 (otomatik)
HomeImageWidth: getEnvAsInt("HOME_IMAGE_W", 800), // Varsayılan 800
HomeImageQuality: getEnvAsInt("HOME_IMAGE_Q", 80), // Varsayılan 80
HomeImageFormat: getEnv("HOME_IMAGE_F", "webp"), // Varsayılan webp
HomeImageMode: getEnv("HOME_IMAGE_B", "contain"), // Varsayılan contain (Fit)
// Hakkında Resim Varsayılanları
AboutImageHeight: getEnvAsInt("ABOUTME_IMAGE_H", getEnvAsInt("ABOUT_IMAGE_H", 0)),
AboutImageWidth: getEnvAsInt("ABOUTME_IMAGE_W", getEnvAsInt("ABOUT_IMAGE_W", 800)),
AboutImageQuality: getEnvAsInt("ABOUTME_IMAGE_Q", getEnvAsInt("ABOUT_IMAGE_Q", 80)),
AboutImageFormat: getEnv("ABOUTME_IMAGE_F", getEnv("ABOUT_IMAGE_F", "webp")),
AboutImageMode: getEnv("ABOUTME_IMAGE_B", getEnv("ABOUT_IMAGE_B", "contain")),
// Servis Resim Varsayılanları
ServiceImageHeight: getEnvAsInt("SERVICE_IMAGE_H", 256),
ServiceImageWidth: getEnvAsInt("SERVICE_IMAGE_W", 256),
ServiceImageQuality: getEnvAsInt("SERVICE_IMAGE_Q", 90),
ServiceImageFormat: getEnv("SERVICE_IMAGE_F", "png"),
ServiceImageMode: getEnv("SERVICE_IMAGE_B", "cover"),
// Gönderi Resim Varsayılanları
PostImageHeight: getEnvAsInt("POST_IMAGE_H", 450),
PostImageWidth: getEnvAsInt("POST_IMAGE_W", 700),
PostImageQuality: getEnvAsInt("POST_IMAGE_Q", 90),
PostImageFormat: getEnv("POST_IMAGE_F", "webp"),
PostImageMode: getEnv("POST_IMAGE_B", "cover"),
// Gönderi Kategori Resim Varsayılanları
PostCategoryImageHeight: getEnvAsInt("POST_CATEGORY_IMAGE_H", 300),
PostCategoryImageWidth: getEnvAsInt("POST_CATEGORY_IMAGE_W", 300),
PostCategoryImageQuality: getEnvAsInt("POST_CATEGORY_IMAGE_Q", 85),
PostCategoryImageFormat: getEnv("POST_CATEGORY_IMAGE_F", "png"),
PostCategoryImageMode: getEnv("POST_CATEGORY_IMAGE_B", "cover"),
// Site Logo Varsayılanları
SettingsLogoHeight: getEnvAsInt("SETTINGS_LOGO_H", 54),
SettingsLogoWidth: getEnvAsInt("SETTINGS_LOGO_W", 165),
SettingsLogoQuality: getEnvAsInt("SETTINGS_LOGO_Q", 85),
SettingsLogoFormat: getEnv("SETTINGS_LOGO_F", "png"),
SettingsLogoMode: getEnv("SETTINGS_LOGO_B", "cover"),
// Afiş Resim Varsayılanları
BannerImageHeight: getEnvAsInt("BANNER_IMAGE_H", 700),
BannerImageWidth: getEnvAsInt("BANNER_IMAGE_W", 1920),
BannerImageQuality: getEnvAsInt("BANNER_IMAGE_Q", 85),
BannerImageFormat: getEnv("BANNER_IMAGE_F", "webp"),
BannerImageMode: getEnv("BANNER_IMAGE_B", "cover"),
// Afiş Küçük Resim (Thumb) Varsayılanları
BannerThumbHeight: getEnvAsInt("BANNER_THUMB_H", 48),
BannerThumbWidth: getEnvAsInt("BANNER_THUMB_W", 48),
BannerThumbQuality: getEnvAsInt("BANNER_THUMB_Q", 90),
BannerThumbFormat: getEnv("BANNER_THUMB_F", "png"),
BannerThumbMode: getEnv("BANNER_THUMB_B", "cover"),
// E-posta Varsayılanları
EmailHost: getEnv("EMAIL_HOST", "localhost"),
EmailPort: getEnv("EMAIL_PORT", "1025"),
EmailHostUser: getEnv("EMAIL_HOST_USER", ""),
EmailHostPassword: getEnv("EMAIL_HOST_PASSWORD", ""),
EmailFrom: getEnv("EMAIL_FROM", "noreply@gauth.local"),
CorsDebug: getEnvAsBool("CORS_DEBUG", false),
}
}
func getEnv(key, fallback string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return fallback
}
func getEnvAsInt(key string, fallback int) int {
valueStr := getEnv(key, "")
if valueStr == "" {
return fallback
}
value, err := strconv.Atoi(valueStr)
if err != nil {
return fallback
}
return value
}
func getEnvAsBool(key string, fallback bool) bool {
valueStr := strings.TrimSpace(getEnv(key, ""))
if valueStr == "" {
return fallback
}
value, err := strconv.ParseBool(valueStr)
if err != nil {
return fallback
}
return value
}

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
}

View File

@@ -0,0 +1,39 @@
package database
import (
configs "goFiber/config"
"log"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func ConnectDB() {
dsn := configs.AppConfig.DBUrl
if dsn == "" {
log.Println(".env dosyasında DB_URL ayarlı değil — veritabanı bağlantısı atlanıyor (geliştirme modu)")
return
}
log.Println("Yapılandırmada DB_URL bulundu, veritabanına bağlanılmaya çalışılıyor...")
// GORM için MySQL konfigürasyonu
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), // Info seviyesi (performans etkileyebilir); üretimde Error seviyesine alınabilir
PrepareStmt: true, // PrepareStmt performansını artırmak için
NowFunc: func() time.Time {
return time.Now().UTC()
},
})
if err != nil {
log.Println("MySQL veritabanı bağlantısı kurulamadı:", err)
return
}
log.Println("MySQL veritabanı bağlantısı kuruldu.")
DB = db
}

108
database/config/redis_db.go Normal file
View File

@@ -0,0 +1,108 @@
package database
import (
"context"
"github.com/redis/go-redis/v9"
config "goFiber/config"
"log"
"time"
)
var RedisClient *redis.Client
var RedisOptions *redis.Options
var ctx = context.Background()
func ConnectRedis() {
redisURL := config.AppConfig.RedisUrl
if redisURL == "" {
log.Println("Warning: REDIS_URL is not set, continuing without Redis cache")
return
}
opt, err := redis.ParseURL(redisURL)
if err != nil {
log.Printf("Warning: Failed to parse Redis URL: %v, continuing without Redis cache", err)
RedisOptions = nil
return
}
RedisOptions = opt
RedisClient = redis.NewClient(opt)
// Test connection
_, err = RedisClient.Ping(ctx).Result()
if err != nil {
log.Printf("Warning: Failed to connect to Redis: %v, continuing without Redis cache", err)
RedisClient = nil
RedisOptions = nil
return
}
log.Println("Connected to Redis successfully")
}
// Set stores a key-value pair in Redis with expiration
func Set(key string, value interface{}, expiration time.Duration) error {
if RedisClient == nil {
return nil // Gracefully handle when Redis is not available
}
return RedisClient.Set(ctx, key, value, expiration).Err()
}
// Get retrieves a value from Redis
func Get(key string) (string, error) {
if RedisClient == nil {
return "", redis.Nil // Return Nil error when Redis is not available
}
return RedisClient.Get(ctx, key).Result()
}
// Delete removes a key from Redis
func Delete(key string) error {
if RedisClient == nil {
return nil
}
return RedisClient.Del(ctx, key).Err()
}
// Exists checks if a key exists in Redis
func Exists(key string) (bool, error) {
if RedisClient == nil {
return false, nil
}
count, err := RedisClient.Exists(ctx, key).Result()
return count > 0, err
}
// SetWithJSON stores a JSON-serializable value in Redis
func SetEx(key string, value interface{}, seconds int) error {
if RedisClient == nil {
return nil
}
return RedisClient.Set(ctx, key, value, time.Duration(seconds)*time.Second).Err()
}
// Increment increments a counter in Redis
func Increment(key string) (int64, error) {
if RedisClient == nil {
return 0, nil
}
return RedisClient.Incr(ctx, key).Result()
}
// Expire sets expiration time for a key
func Expire(key string, expiration time.Duration) error {
if RedisClient == nil {
return nil
}
return RedisClient.Expire(ctx, key, expiration).Err()
}
// FlushAll clears all keys in the current database
func FlushAll() error {
if RedisClient == nil {
return nil
}
log.Println("🧹 Clearing Redis Cache...")
return RedisClient.FlushDB(ctx).Err()
}

122
database/migrate/migrate.go Normal file
View File

@@ -0,0 +1,122 @@
package migrasyon
import (
configs "goFiber/config"
database "goFiber/database/config"
"goFiber/database/models"
"log"
"net/url"
"strings"
)
// Only run AutoMigrate if DB is initialized
func Migrate() {
if database.DB != nil {
if err := database.DB.AutoMigrate(
&models.User{},
&models.SocialAccount{},
&models.Profile{},
&models.Hero{},
&models.Setting{},
&models.CorsWhitelist{},
&models.CorsBlacklist{},
&models.RateLimitSetting{},
&models.Category{},
&models.Tag{},
&models.Post{},
&models.CategoryView{},
&models.Comment{},
); err != nil {
log.Printf("AutoMigrate Yapılamadı !!: %v", err)
}
seedSecurityDefaults()
log.Println("AutoMigrate Yapıldı.")
} else {
log.Println("DB not initialized: skipping AutoMigrate")
}
}
func seedSecurityDefaults() {
seedRateLimit("register", "Register endpoint default rate limit", 5, 60)
seedRateLimit("login", "Login endpoint default rate limit", 10, 60)
for _, origin := range defaultWhitelistOrigins() {
seedCorsWhitelist(origin, "default seeded whitelist")
}
}
func seedRateLimit(name, description string, maxRequests int64, windowSeconds int) {
var existing models.RateLimitSetting
if err := database.DB.Where("name = ?", name).First(&existing).Error; err == nil {
return
}
item := models.RateLimitSetting{
Name: name,
Description: description,
MaxRequests: maxRequests,
WindowSeconds: windowSeconds,
IsActive: true,
UpdatedBy: "seed",
}
if err := database.DB.Create(&item).Error; err != nil {
log.Printf("RateLimit seed failed (%s): %v", name, err)
return
}
log.Printf("RateLimit seed created: name=%s max=%d window=%ds", name, maxRequests, windowSeconds)
}
func seedCorsWhitelist(origin, description string) {
origin = strings.TrimSpace(origin)
if origin == "" {
return
}
var existing models.CorsWhitelist
if err := database.DB.Where("origin = ?", origin).First(&existing).Error; err == nil {
return
}
item := models.CorsWhitelist{
Origin: origin,
Description: description,
IsActive: true,
CreatedBy: "seed",
}
if err := database.DB.Create(&item).Error; err != nil {
log.Printf("CorsWhitelist seed failed (%s): %v", origin, err)
return
}
log.Printf("CorsWhitelist seed created: origin=%s", origin)
}
func defaultWhitelistOrigins() []string {
origins := []string{
"http://localhost:3000",
"http://localhost:5173",
"http://localhost:8080",
}
appURL := strings.TrimSpace(configs.AppConfig.AppURL)
if appURL != "" {
if parsed, err := url.Parse(appURL); err == nil && parsed.Scheme != "" && parsed.Host != "" {
origins = append(origins, parsed.Scheme+"://"+parsed.Host)
}
}
uniq := make(map[string]struct{})
out := make([]string, 0, len(origins))
for _, origin := range origins {
origin = strings.TrimSpace(origin)
if origin == "" {
continue
}
if _, ok := uniq[origin]; ok {
continue
}
uniq[origin] = struct{}{}
out = append(out, origin)
}
return out
}

47
database/models/blog.go Normal file
View File

@@ -0,0 +1,47 @@
package models
import (
"gorm.io/gorm"
)
// Minimal, temiz GORM modelleri
type Category struct {
gorm.Model
Title string `gorm:"type:varchar(254);not null" json:"title"`
Slug string `gorm:"type:varchar(254);not null;uniqueIndex" json:"slug"`
Description string `json:"description,omitempty"`
ParentID *uint `json:"parent_id,omitempty"`
Parent *Category `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;foreignKey:ParentID" json:"parent,omitempty"`
Children []Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
Posts []Post `gorm:"many2many:post_categories;" json:"posts,omitempty"`
}
type Tag struct {
gorm.Model
Name string `gorm:"type:varchar(254);not null" json:"name"`
Posts []Post `gorm:"many2many:post_tags;" json:"posts,omitempty"`
}
type Post struct {
gorm.Model
Title string `gorm:"type:varchar(254);not null" json:"title"`
Images string `gorm:"type:text;not null" json:"images"`
Content string `gorm:"type:text" json:"content,omitempty"`
Slug string `gorm:"type:varchar(254);not null;uniqueIndex" json:"slug"`
Categories []Category `gorm:"many2many:post_categories;" json:"categories,omitempty"`
Tags []Tag `gorm:"many2many:post_tags;" json:"tags,omitempty"`
}
type CategoryView struct {
gorm.Model
CategoryID uint `json:"category_id"`
IPAddress string `gorm:"type:varchar(45)" json:"ip_address,omitempty"`
}
type Comment struct {
gorm.Model
UserID uint `json:"user_id"`
PostID uint `json:"post_id"`
Body string `gorm:"type:text" json:"body,omitempty"`
}

34
database/models/cors.go Normal file
View File

@@ -0,0 +1,34 @@
package models
import (
"gorm.io/gorm"
)
// CorsWhitelist - CORS için izin verilen origin'ler
type CorsWhitelist struct {
gorm.Model
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
Description string `gorm:"type:varchar(255)" json:"description"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
}
// CorsBlacklist - CORS için yasaklanan origin'ler
type CorsBlacklist struct {
gorm.Model
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
Reason string `gorm:"type:varchar(255)" json:"reason"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
}
// RateLimitSetting - Rate limit ayarları
type RateLimitSetting struct {
gorm.Model
Name string `gorm:"type:varchar(100);uniqueIndex;not null" json:"name"` // e.g., "login", "register", "api"
Description string `gorm:"type:varchar(255)" json:"description"`
MaxRequests int64 `gorm:"not null" json:"max_requests"` // Max istek sayısı
WindowSeconds int `gorm:"not null" json:"window_seconds"` // Zaman penceresi (saniye)
IsActive bool `gorm:"default:true" json:"is_active"`
UpdatedBy string `gorm:"type:varchar(255)" json:"updated_by,omitempty"`
}

View File

@@ -0,0 +1,39 @@
package models
// Swagger-friendly (light) structs for documentation only.
// These avoid embedding external types (gorm.Model) so `swag` can parse them.
type CategoryDoc struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
ParentID *uint `json:"parent_id,omitempty"`
Children []CategoryDoc `json:"children,omitempty"`
}
type TagDoc struct {
ID uint `json:"id"`
Name string `json:"name"`
}
type PostDoc struct {
ID uint `json:"id"`
Title string `json:"title"`
Content string `json:"content,omitempty"`
Images []string `json:"images,omitempty"`
Categories []CategoryDoc `json:"categories,omitempty"`
Tags []TagDoc `json:"tags,omitempty"`
}
type CommentDoc struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
PostID uint `json:"post_id"`
Body string `json:"body,omitempty"`
}
type CategoryViewDoc struct {
ID uint `json:"id"`
CategoryID uint `json:"category_id"`
IPAddress string `json:"ip_address,omitempty"`
}

19
database/models/hero.go Normal file
View File

@@ -0,0 +1,19 @@
package models
import (
"gorm.io/gorm"
)
// Banner model structure
// Represents a banner item with optional thumbnail.
type Hero struct {
gorm.Model
Color string `gorm:"type:varchar(32);not null" json:"color" form:"color"`
Title string `gorm:"type:varchar(254)" json:"title,omitempty" form:"title"`
Text1 string `gorm:"type:varchar(254)" json:"text1,omitempty" form:"text1"`
Text2 string `gorm:"type:varchar(254)" json:"text2,omitempty" form:"text2"`
Text4 string `gorm:"type:varchar(254)" json:"text4,omitempty" form:"text4"`
Text5 string `gorm:"type:varchar(254)" json:"text5,omitempty" form:"text5"`
Image string `gorm:"type:varchar(254)" json:"image" form:"image"`
IsActive bool `gorm:"default:true" json:"is_active" form:"is_active"`
}

View File

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

48
database/models/user.go Normal file
View File

@@ -0,0 +1,48 @@
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
gorm.Model
UserName string `json:"username" gorm:"type:varchar(255)"`
Email string `gorm:"uniqueIndex;not null;type:varchar(255)" json:"email"`
Password string `json:"-" gorm:"type:varchar(255)"` // Password shouldn't be returned in JSON
EmailVerified *bool `gorm:"default:false" json:"email_verified"` // default false for email/password registration
EmailVerifyToken string `gorm:"index;type:varchar(255)" json:"-"`
EmailVerifiedAt *time.Time `json:"email_verified_at,omitempty"`
IsAdmin *bool `gorm:"default:false" json:"is_admin"`
SocialAccounts []SocialAccount `gorm:"foreignKey:UserID" json:"social_accounts,omitempty"`
Profile []Profile `gorm:"foreignKey:UserID" json:"profiles,omitempty"`
}
// Email Veriyf i False Döndürüyor
func (u *User) IsEmailVerified() bool {
if u.EmailVerified == nil {
return false
}
return *u.EmailVerified
}
// SocialAccount model structure
type SocialAccount struct {
gorm.Model
UserID uint64 `gorm:"type:bigint unsigned;not null;index" json:"user_id"`
Provider string `gorm:"not null" json:"provider"` // google, github
ProviderID string `gorm:"not null" json:"provider_id"`
Email string `json:"email" gorm:"type:varchar(255)"`
Name string `json:"name,omitempty" gorm:"type:varchar(255)"` // Full name from provider
AvatarURL string `json:"avatar_url,omitempty" gorm:"type:varchar(255)"` // Avatar URL from provider
}
type Profile struct {
gorm.Model
UserID uint64 `gorm:"type:bigint unsigned;not null;index" json:"user_id"`
AvatarURL string `json:"avatar_url,omitempty" gorm:"type:varchar(255)"` // Avatar URL from provider
FirstName string `json:"first_name" gorm:"type:varchar(255)"` // Full name from provider
LastName string `json:"last_name" gorm:"type:varchar(255)"` // Full name from provider
}

29
docker-compose.c.yml Normal file
View File

@@ -0,0 +1,29 @@
version: '3.8'
services:
app:
build:
context: .
args:
DB_URL: ${DB_URL}
REDIS_URL: ${REDIS_URL}
REDIS_HOST: ${REDIS_HOST}
REDIS_PORT: ${REDIS_PORT}
EMAIL_HOST: ${EMAIL_HOST}
EMAIL_PORT: ${EMAIL_PORT}
container_name: gofiber-app
#ports:
# - "8080:8080"
env_file:
- .env
volumes:
- gofiber_uploads:/app/uploads
- gofiber_views:/app/views
- gofiber_docs:/app/docs
restart: unless-stopped
# Define named volumes used above
volumes:
gofiber_uploads: {}
gofiber_views: {}
gofiber_docs: {}

29
docker-compose.yml Normal file
View File

@@ -0,0 +1,29 @@
version: '3.8'
services:
app:
build:
context: .
args:
DB_URL: ${DB_URL}
REDIS_URL: ${REDIS_URL}
REDIS_HOST: ${REDIS_HOST}
REDIS_PORT: ${REDIS_PORT}
EMAIL_HOST: ${EMAIL_HOST}
EMAIL_PORT: ${EMAIL_PORT}
container_name: gofiber-app
#ports:
# - "8080:8080"
env_file:
- .env
volumes:
- ./uploads:/app/uploads
- ./views:/app/views
- ./docs:/app/docs
networks:
- dokploy-network
restart: unless-stopped
networks:
dokploy-network:
external: true

4110
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

4089
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

2633
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

71
go.mod Normal file
View File

@@ -0,0 +1,71 @@
module goFiber
go 1.25.0
require (
github.com/go-playground/validator/v10 v10.30.1
github.com/gofiber/fiber/v3 v3.0.0
github.com/gofiber/storage/redis/v3 v3.4.3
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/v9 v9.17.3
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.6
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.48.0
golang.org/x/sys v0.41.0 // indirect
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1
)
require github.com/prometheus/client_golang v1.23.2
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/gofiber/contrib/fiberzap/v2 v2.1.6 // indirect
github.com/gofiber/fiber/v2 v2.52.6 // indirect
github.com/gofiber/schema v1.7.0 // indirect
github.com/gofiber/utils/v2 v2.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
github.com/tinylib/msgp v1.6.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

276
go.sum Normal file
View File

@@ -0,0 +1,276 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gofiber/contrib/fiberzap/v2 v2.1.6 h1:8aMBaO7jAB4w9o2uGC1S3ieKPxg8vfJ7t1aipq2pudg=
github.com/gofiber/contrib/fiberzap/v2 v2.1.6/go.mod h1:sGrPV2XzRrI6aJQOmORr5rdk4vXLR630Oc/REtMmCYs=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/fiber/v3 v3.0.0 h1:GPeCG8X60L42wLKrzgeewDHBr6pE6veAvwaXsqD3Xjk=
github.com/gofiber/fiber/v3 v3.0.0/go.mod h1:kVZiO/AwyT5Pq6PgC8qRCJ+j/BHrMy5jNw1O9yH38aY=
github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=
github.com/gofiber/schema v1.7.0/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
github.com/gofiber/storage/redis/v3 v3.4.3 h1:PvazbTpDAvmDHpMk4fCvCoTXm+neLXQL1rWuHTXlNz8=
github.com/gofiber/storage/redis/v3 v3.4.3/go.mod h1:n/wFsaS4cwfRQERwhkZhMmJrNFAf514MaWL7ky33sTk=
github.com/gofiber/storage/testhelpers/redis v0.1.0 h1:lDUwtanDf3f5YwlDwhbqnqCtj9Y/xc8ctxRE6HpQcws=
github.com/gofiber/storage/testhelpers/redis v0.1.0/go.mod h1:Y1UccxbGVL04+TF5RuyCsksX+76hu6nJIWjPukBBgJ4=
github.com/gofiber/utils/v2 v2.0.1 h1:+kvhvoGuAeUBzF/Qlkx5HvFK7tNd62mxSpBuI0zCRII=
github.com/gofiber/utils/v2 v2.0.1/go.mod h1:xF9v89FfmbrYqI/bQUGN7gR8ZtXot2jxnZvmAUtiavE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shamaton/msgpack/v3 v3.0.0 h1:xl40uxWkSpwBCSTvS5wyXvJRsC6AcVcYeox9PspKiZg=
github.com/shamaton/msgpack/v3 v3.0.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc=
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
github.com/testcontainers/testcontainers-go/modules/redis v0.40.0 h1:OG4qwcxp2O0re7V7M9lY9w0v6wWgWf7j7rtkpAnGMd0=
github.com/testcontainers/testcontainers-go/modules/redis v0.40.0/go.mod h1:Bc+EDhKMo5zI5V5zdBkHiMVzeAXbtI4n5isS/nzf6zw=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

116
main.go Normal file
View File

@@ -0,0 +1,116 @@
package main
import (
"fmt"
configs "goFiber/config"
database "goFiber/database/config"
migrasyon "goFiber/database/migrate"
"goFiber/middlewares"
"goFiber/routes"
"log"
"time"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/adaptor"
"github.com/gofiber/fiber/v3/middleware/session"
redisStorage "github.com/gofiber/storage/redis/v3"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
)
var SessionStore *session.Store
func init() {
configs.LoadConfig()
database.ConnectDB()
database.ConnectRedis()
migrasyon.Migrate()
// Session store: Redis varsa Redis-backed storage, yoksa in-memory
if database.RedisOptions != nil {
// Use URL so storage package parses it correctly
cfg := redisStorage.Config{
URL: configs.AppConfig.RedisUrl,
}
// If TLSConfig was set during parse, copy it over
if database.RedisOptions.TLSConfig != nil {
cfg.TLSConfig = database.RedisOptions.TLSConfig
}
storage := redisStorage.New(cfg)
SessionStore = session.NewStore(session.Config{
Storage: storage,
})
log.Println("Session store: using Redis-backed storage")
} else {
// Basit: her durumda in-memory session store kullan (opsiyonel: Redis kullanımı ileride eklenebilir)
SessionStore = session.NewStore()
log.Println("Session store: using in-memory storage")
}
log.Println("Init Uygulandı !!")
}
// @title AreS Fiber API Server
// @version 1.0
// @description This is a sample server for AreS Fiber API.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /
// @schemes http
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
func main() {
logger, _ := zap.NewProduction()
zap.ReplaceGlobals(logger)
defer logger.Sync()
sugar := logger.Sugar()
app := fiber.New(fiber.Config{
AppName: "AreS Fiber API Server",
IdleTimeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
Concurrency: 256 * 1024,
})
//zap.L().Info("Fiber uygulaması başlatıldı")
// prevent 'unused variable' warning for SessionStore (it may be used elsewhere)
_ = SessionStore
app.Get("/metric", adaptor.HTTPHandler(promhttp.Handler()))
app.Get("/health", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "ok",
})
})
// Serve uploads folder dynamically so URLs like /uploads/settings/xxx.png are accessible
app.Get("/uploads/*", func(c fiber.Ctx) error {
file := c.Params("*")
return c.SendFile("./uploads/" + file)
})
app.Use(middlewares.DynamicCORS())
routes.RouterUser(app)
port := configs.AppConfig.Port
if port == "" {
port = "8080" // Fallback port
}
sugar.Info("Server Bu Porta Başladı: " + port)
_ = app.Listen(fmt.Sprintf(":%s", port))
}

View File

@@ -0,0 +1,63 @@
package middlewares
import (
"strings"
"goFiber/services"
"github.com/gofiber/fiber/v3"
)
const authClaimsKey = "auth_claims"
func RequireAuth(c fiber.Ctx) error {
authHeader := strings.TrimSpace(c.Get("Authorization"))
if authHeader == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "authorization header is required"})
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || strings.TrimSpace(parts[1]) == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid authorization format, expected: Bearer <token>"})
}
jwtService := services.NewJWTService()
claims, err := jwtService.ValidateToken(strings.TrimSpace(parts[1]))
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid token"})
}
if claims.TokenType != services.TokenTypeAccess {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "access token required"})
}
c.Locals(authClaimsKey, claims)
return c.Next()
}
func RequireAdmin(c fiber.Ctx) error {
claims, ok := GetAuthClaims(c)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
if !claims.IsAdmin {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "admin role required"})
}
return c.Next()
}
func RequireNormalUser(c fiber.Ctx) error {
claims, ok := GetAuthClaims(c)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
if claims.IsAdmin {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "only normal users can access this endpoint"})
}
return c.Next()
}
func GetAuthClaims(c fiber.Ctx) (*services.JWTClaim, bool) {
raw := c.Locals(authClaimsKey)
claims, ok := raw.(*services.JWTClaim)
return claims, ok
}

137
middlewares/dynamic_cors.go Normal file
View File

@@ -0,0 +1,137 @@
package middlewares
import (
"encoding/json"
"errors"
configs "goFiber/config"
database "goFiber/database/config"
"goFiber/database/models"
"log"
"net/http"
"strings"
"github.com/gofiber/fiber/v3"
"github.com/redis/go-redis/v9"
)
const (
corsWhitelistActiveCacheKey = "cors:active:whitelist"
corsBlacklistActiveCacheKey = "cors:active:blacklist"
corsCacheTTLSeconds = 60
)
var (
allowedMethods = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
allowedHeaders = "Authorization,Content-Type,Accept,Origin,X-Requested-With"
)
// DynamicCORS validates request Origin using DB-backed whitelist/blacklist with Redis caching.
func DynamicCORS() fiber.Handler {
return func(c fiber.Ctx) error {
origin := strings.TrimSpace(c.Get("Origin"))
if origin == "" {
return c.Next()
}
if database.DB == nil {
corsLogf("[cors][skip] database unavailable origin=%s path=%s", origin, c.Path())
return c.Next()
}
originKey := strings.ToLower(origin)
// Keep same-origin requests working even if DB entries are missing.
if origin == requestBaseURL(c) {
corsLogf("[cors][allow] same-origin origin=%s path=%s", origin, c.Path())
setCORSHeaders(c, origin)
if c.Method() == http.MethodOptions {
return c.SendStatus(http.StatusNoContent)
}
return c.Next()
}
blacklist, err := loadActiveOriginSet(corsBlacklistActiveCacheKey, true)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cors blacklist lookup failed"})
}
if blacklist[originKey] {
log.Printf("[cors][blocked] blacklist origin=%s path=%s", origin, c.Path())
return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": "origin is blocked by CORS policy"})
}
whitelist, err := loadActiveOriginSet(corsWhitelistActiveCacheKey, false)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cors whitelist lookup failed"})
}
if !whitelist[originKey] {
log.Printf("[cors][blocked] not-whitelisted origin=%s path=%s", origin, c.Path())
return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": "origin is not allowed by CORS policy"})
}
corsLogf("[cors][allow] origin=%s path=%s", origin, c.Path())
setCORSHeaders(c, origin)
if c.Method() == http.MethodOptions {
return c.SendStatus(http.StatusNoContent)
}
return c.Next()
}
}
func setCORSHeaders(c fiber.Ctx, origin string) {
c.Set("Vary", "Origin")
c.Set("Access-Control-Allow-Origin", origin)
c.Set("Access-Control-Allow-Methods", allowedMethods)
c.Set("Access-Control-Allow-Headers", allowedHeaders)
c.Set("Access-Control-Allow-Credentials", "true")
c.Set("Access-Control-Max-Age", "600")
}
func requestBaseURL(c fiber.Ctx) string {
return c.Protocol() + "://" + c.Get("Host")
}
func loadActiveOriginSet(cacheKey string, isBlacklist bool) (map[string]bool, error) {
out := make(map[string]bool)
if cached, err := database.Get(cacheKey); err == nil {
corsLogf("[cors][cache-hit] key=%s", cacheKey)
var origins []string
if jsonErr := json.Unmarshal([]byte(cached), &origins); jsonErr == nil {
for _, origin := range origins {
out[strings.ToLower(strings.TrimSpace(origin))] = true
}
return out, nil
}
} else if !errors.Is(err, redis.Nil) {
return nil, err
}
corsLogf("[cors][cache-miss] key=%s", cacheKey)
var origins []string
var dbErr error
if isBlacklist {
dbErr = database.DB.Model(&models.CorsBlacklist{}).
Where("is_active = ?", true).
Pluck("origin", &origins).Error
} else {
dbErr = database.DB.Model(&models.CorsWhitelist{}).
Where("is_active = ?", true).
Pluck("origin", &origins).Error
}
if dbErr != nil {
return nil, dbErr
}
for _, origin := range origins {
out[strings.ToLower(strings.TrimSpace(origin))] = true
}
cacheBytes, _ := json.Marshal(origins)
_ = database.SetEx(cacheKey, string(cacheBytes), corsCacheTTLSeconds)
return out, nil
}
func corsLogf(format string, args ...interface{}) {
if configs.AppConfig != nil && configs.AppConfig.CorsDebug {
log.Printf(format, args...)
}
}

122
middlewares/rate_limit.go Normal file
View File

@@ -0,0 +1,122 @@
package middlewares
import (
"context"
"encoding/json"
"errors"
"fmt"
configs "goFiber/config"
database "goFiber/database/config"
"goFiber/database/models"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v3"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type rateLimitRuntime struct {
Name string `json:"name"`
MaxRequests int64 `json:"max_requests"`
WindowSeconds int `json:"window_seconds"`
IsActive bool `json:"is_active"`
}
// RequireRateLimit applies Redis-backed per-IP rate limiting by setting name.
func RequireRateLimit(name string, fallbackMax int64, fallbackWindowSeconds int) fiber.Handler {
return func(c fiber.Ctx) error {
if database.DB == nil {
return c.Next()
}
setting, err := loadRateLimitRuntime(name, fallbackMax, fallbackWindowSeconds)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "rate limit configuration error"})
}
if !setting.IsActive {
return c.Next()
}
if database.RedisClient == nil {
rateLimitLogf("[rate-limit][warn] redis unavailable, skipping enforcement name=%s", setting.Name)
return c.Next()
}
ip := strings.TrimSpace(c.IP())
if ip == "" {
ip = "unknown"
}
counterKey := fmt.Sprintf("ratelimit:%s:%s", setting.Name, ip)
count, err := database.RedisClient.Incr(context.Background(), counterKey).Result()
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "rate limit check failed"})
}
if count == 1 {
_ = database.RedisClient.Expire(context.Background(), counterKey, time.Duration(setting.WindowSeconds)*time.Second).Err()
}
if count > setting.MaxRequests {
ttl, _ := database.RedisClient.TTL(context.Background(), counterKey).Result()
retryAfter := int(ttl.Seconds())
if retryAfter < 1 {
retryAfter = setting.WindowSeconds
}
c.Set("Retry-After", strconv.Itoa(retryAfter))
log.Printf("[rate-limit][blocked] name=%s ip=%s count=%d max=%d window=%ds", setting.Name, ip, count, setting.MaxRequests, setting.WindowSeconds)
return c.Status(http.StatusTooManyRequests).JSON(fiber.Map{
"error": "too many requests",
"retry_after": retryAfter,
})
}
rateLimitLogf("[rate-limit][allow] name=%s ip=%s count=%d max=%d window=%ds", setting.Name, ip, count, setting.MaxRequests, setting.WindowSeconds)
return c.Next()
}
}
func loadRateLimitRuntime(name string, fallbackMax int64, fallbackWindowSeconds int) (*rateLimitRuntime, error) {
cacheKey := "ratelimit:setting:" + name
if cached, err := database.Get(cacheKey); err == nil {
var s rateLimitRuntime
if jsonErr := json.Unmarshal([]byte(cached), &s); jsonErr == nil {
return &s, nil
}
} else if !errors.Is(err, redis.Nil) {
return nil, err
}
setting := &rateLimitRuntime{
Name: name,
MaxRequests: fallbackMax,
WindowSeconds: fallbackWindowSeconds,
IsActive: true,
}
var dbSetting models.RateLimitSetting
if err := database.DB.Where("name = ?", name).First(&dbSetting).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
rateLimitLogf("[rate-limit][config] setting=%s not found, using fallback max=%d window=%ds", name, fallbackMax, fallbackWindowSeconds)
} else {
setting.MaxRequests = dbSetting.MaxRequests
setting.WindowSeconds = dbSetting.WindowSeconds
setting.IsActive = dbSetting.IsActive
rateLimitLogf("[rate-limit][config] loaded from db name=%s active=%t max=%d window=%ds", name, setting.IsActive, setting.MaxRequests, setting.WindowSeconds)
}
cacheJSON, _ := json.Marshal(setting)
_ = database.SetEx(cacheKey, string(cacheJSON), 60)
return setting, nil
}
func rateLimitLogf(format string, args ...interface{}) {
if configs.AppConfig != nil && configs.AppConfig.CorsDebug {
log.Printf(format, args...)
}
}

15
pkg/utis/token.go Normal file
View File

@@ -0,0 +1,15 @@
package utils
import (
"crypto/rand"
"encoding/hex"
)
// GenerateSecureToken returns a cryptographically random hex string (e.g. for email verification).
func GenerateSecureToken(byteLength int) (string, error) {
b := make([]byte, byteLength)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}

53
rest.client Normal file
View File

@@ -0,0 +1,53 @@
### Get all heroes (no auth)
GET http://localhost:8080/api/v1/heroes
Accept: application/json
### Get active heroes (no auth)
GET http://localhost:8080/api/v1/hero
Accept: application/json
### Update hero (JSON) — requires admin token
PUT http://localhost:8080/api/v1/hero/1
Content-Type: application/json
Authorization: Bearer {{ADMIN_TOKEN}}
{
"title": "updated-via-rest",
"is_active": false
}
### Update hero (multipart/form-data) — send file + is_active=false
PUT http://localhost:8080/api/v1/hero/1
Authorization: Bearer {{ADMIN_TOKEN}}
Content-Type: multipart/form-data; boundary=---011000010111000001101001
-----011000010111000001101001
Content-Disposition: form-data; name="title"
multipart-update
-----011000010111000001101001
Content-Disposition: form-data; name="is_active"
false
-----011000010111000001101001
Content-Disposition: form-data; name="image"; filename="test.jpg"
Content-Type: image/jpeg
< ./path/to/test.jpg
-----011000010111000001101001--
### Delete hero (admin)
DELETE http://localhost:8080/api/v1/hero/1
Authorization: Bearer {{ADMIN_TOKEN}}
---
# Equivalent curl examples:
#
# curl GET all heroes
# curl -sS http://localhost:8080/api/v1/heroes | jq '.'
#
# curl update JSON
# curl -X PUT "http://localhost:8080/api/v1/hero/1" -H "Authorization: Bearer <ADMIN_TOKEN>" -H "Content-Type: application/json" -d '{"title":"test","is_active":false}'
#
# curl multipart (with image)
# curl -X PUT "http://localhost:8080/api/v1/hero/1" -H "Authorization: Bearer <ADMIN_TOKEN>" -F "title=multipart-test" -F "is_active=false" -F "image=@/absolute/path/to/image.jpg"

146
routes/router.go Normal file
View File

@@ -0,0 +1,146 @@
package routes
import (
"goFiber/controllers"
"goFiber/middlewares"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/adaptor"
httpSwagger "github.com/swaggo/http-swagger"
)
func RouterUser(app *fiber.App) {
app.Get("/", func(c fiber.Ctx) error {
return c.SendFile("./views/coming_soon.html")
})
app.Get("/swagger/doc.json", func(c fiber.Ctx) error {
return c.SendFile("./docs/swagger.json")
})
app.Get("/swagger/*", adaptor.HTTPHandler(httpSwagger.Handler(
httpSwagger.URL("/swagger/doc.json"),
httpSwagger.PersistAuthorization(true),
httpSwagger.UIConfig(map[string]string{
"requestInterceptor": `function(req) {
const auth = req.headers.Authorization || req.headers.authorization;
if (typeof auth === "string" && auth.length > 0 && !auth.toLowerCase().startsWith("bearer ")) {
req.headers.Authorization = "Bearer " + auth;
}
return req;
}`,
}),
)))
api := app.Group("/api/v1")
users := api.Group("/users")
auth := api.Group("/auth")
admin := api.Group("/admin")
//users.Get("/", controllers.GetUser)
usersProtected := users.Group("", middlewares.RequireAuth)
usersProtected.Get("/me", controllers.Me)
usersProtected.Get("/admin/example", middlewares.RequireAdmin, controllers.AdminOnlyExample)
usersProtected.Get("/list", middlewares.RequireAdmin, controllers.AdminListUsers)
usersProtected.Get("/list/deleted", middlewares.RequireAdmin, controllers.AdminListDeletedUsers)
usersProtected.Get("/user/example", middlewares.RequireNormalUser, controllers.UserOnlyExample)
users.Get("/:id", controllers.GetUserOne)
users.Put("/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.UpdateUser)
users.Delete("/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.DeleteUser)
users.Delete("/:id/hard", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.HardDeleteUser)
users.Post("/:id/restore", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.RestoreUser)
auth.Post("/register", middlewares.RequireRateLimit("register", 5, 60), controllers.Register)
auth.Post("/login", middlewares.RequireRateLimit("login", 10, 60), controllers.Login)
auth.Post("/refresh", controllers.RefreshToken, middlewares.RequireRateLimit("refresh", 10, 60), controllers.RefreshToken)
auth.Post("/resend-verification", controllers.ResendVerificationEmail)
auth.Get("/verify-email", controllers.VerifyEmail)
auth.Get("/google", controllers.GoogleAuth)
auth.Get("/google/callback", controllers.GoogleAuthCallback)
auth.Get("/github", controllers.GithubAuth)
auth.Get("/github/callback", controllers.GithubAuthCallback)
// Hero Routes
api.Get("/hero", controllers.GetHero)
api.Get("/heroes", controllers.GetHeroAll)
api.Get("/setting", controllers.GetSetting)
// Blog/Public Routes
api.Get("/posts", controllers.GetPosts)
api.Get("/posts/:id", controllers.GetPost)
api.Get("/categories", controllers.ListCategories)
api.Get("/tags", controllers.ListTags)
api.Get("/comments", controllers.ListComments)
// Blog/Admin Routes
api.Post("/posts", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.CreatePost)
api.Put("/posts/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.UpdatePost)
api.Delete("/posts/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.DeletePost)
// Admin list posts (include trashed filter)
admin.Get("/posts", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.AdminListPosts)
admin.Delete("/posts/:id/hard", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.HardDeletePost)
admin.Post("/posts/:id/restore", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.AdminRestorePost)
// Admin tags operations (list including trashed, hard delete, restore)
admin.Get("/tags", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.AdminListTags)
admin.Delete("/tags/:id/hard", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.HardDeleteTag)
admin.Post("/tags/:id/restore", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.AdminRestoreTag)
// Admin category-views operations
admin.Get("/category-views", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.AdminListCategoryViews)
admin.Delete("/category-views/:id/hard", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.HardDeleteCategoryView)
admin.Post("/category-views/:id/restore", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.AdminRestoreCategoryView)
// Admin categories operations (list including trashed, hard delete, restore)
admin.Get("/categories", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.AdminListCategories)
admin.Delete("/categories/:id/hard", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.HardDeleteCategory)
admin.Post("/categories/:id/restore", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.AdminRestoreCategory)
api.Post("/categories", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.CreateCategory)
api.Put("/categories/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.UpdateCategory)
api.Delete("/categories/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.DeleteCategory)
api.Post("/tags", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.CreateTag)
api.Put("/tags/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.UpdateTag)
api.Delete("/tags/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.DeleteTag)
api.Post("/comments", controllers.CreateComment) // public
api.Delete("/comments/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.DeleteComment)
// Auth Middleware Group
authProtected := auth.Group("", middlewares.RequireAuth)
authProtected.Get("/me", controllers.Me)
//authProtected.Get("/admin/example", middlewares.RequireAdmin, controllers.AdminOnlyExample)
//authProtected.Get("/user/example", middlewares.RequireNormalUser, controllers.UserOnlyExample)
// Admin Hero Operations
api.Post("/hero", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.CreateHero)
api.Put("/hero/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.UpdateHero)
api.Delete("/hero/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.DeleteHero)
// Admin Setting Operations
api.Post("/setting", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.CreateSetting)
api.Put("/setting/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.UpdateSetting)
api.Delete("/setting/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.DeleteSetting)
// Admin Security (CORS & Rate Limit) Operations - internal use only
admin.Get("/cors/whitelist", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.ListCorsWhitelists)
admin.Post("/cors/whitelist", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.CreateCorsWhitelist)
admin.Put("/cors/whitelist/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.UpdateCorsWhitelist)
admin.Delete("/cors/whitelist/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.DeleteCorsWhitelist)
admin.Delete("/cors/whitelist/:id/hard", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.HardDeleteCorsWhitelist)
admin.Get("/cors/blacklist", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.ListCorsBlacklists)
admin.Post("/cors/blacklist", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.CreateCorsBlacklist)
admin.Put("/cors/blacklist/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.UpdateCorsBlacklist)
admin.Delete("/cors/blacklist/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.DeleteCorsBlacklist)
admin.Delete("/cors/blacklist/:id/hard", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.HardDeleteCorsBlacklist)
admin.Get("/rate-limit", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.ListRateLimitSettings)
admin.Post("/rate-limit", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.CreateRateLimitSetting)
admin.Put("/rate-limit/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.UpdateRateLimitSetting)
admin.Delete("/rate-limit/:id", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.DeleteRateLimitSetting)
admin.Delete("/rate-limit/:id/hard", middlewares.RequireAuth, middlewares.RequireAdmin, controllers.HardDeleteRateLimitSetting)
}

233
scripts/seed.go Normal file
View File

@@ -0,0 +1,233 @@
package main
import (
crand "crypto/rand"
"encoding/hex"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
configs "goFiber/config"
database "goFiber/database/config"
"goFiber/database/models"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var nonSlugChars = regexp.MustCompile(`[^a-z0-9\-]+`)
var multiDash = regexp.MustCompile(`-+`)
func randHex(n int) string {
b := make([]byte, n)
_, _ = crand.Read(b)
return hex.EncodeToString(b)[:n]
}
func ensureUploadsDir() error {
p := "./uploads/posts"
return os.MkdirAll(p, 0755)
}
func slugify(s string) string {
repl := strings.NewReplacer(
"ç", "c", "Ç", "c",
"ğ", "g", "Ğ", "g",
"ı", "i", "İ", "i",
"ö", "o", "Ö", "o",
"ş", "s", "Ş", "s",
"ü", "u", "Ü", "u",
)
s = repl.Replace(strings.TrimSpace(strings.ToLower(s)))
s = strings.ReplaceAll(s, " ", "-")
s = nonSlugChars.ReplaceAllString(s, "-")
s = multiDash.ReplaceAllString(s, "-")
s = strings.Trim(s, "-")
if s == "" {
return "item"
}
return s
}
func ensureUniqueCategorySlug(db *gorm.DB, base string) string {
slug := base
i := 1
for {
var count int64
_ = db.Model(&models.Category{}).Where("slug = ?", slug).Count(&count).Error
if count == 0 {
return slug
}
i++
slug = fmt.Sprintf("%s-%d", base, i)
}
}
func ensureUniquePostSlug(db *gorm.DB, base string) string {
slug := base
i := 1
for {
var count int64
_ = db.Model(&models.Post{}).Where("slug = ?", slug).Count(&count).Error
if count == 0 {
return slug
}
i++
slug = fmt.Sprintf("%s-%d", base, i)
}
}
func downloadImage(destDir string, idx int) (string, error) {
url := fmt.Sprintf("https://picsum.photos/1200/800?random=%d", idx)
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
filename := fmt.Sprintf("post_%d_%s.jpg", idx, randHex(6))
outPath := filepath.Join(destDir, filename)
out, err := os.Create(outPath)
if err != nil {
return "", err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return "", err
}
return "uploads/posts/" + filename, nil
}
func main() {
fmt.Println("Seeder starting...")
// load config to get DB_URL
configs.LoadConfig()
// ensure uploads dir
if err := ensureUploadsDir(); err != nil {
fmt.Println("failed to create uploads dir:", err)
os.Exit(1)
}
// Require DB_URL / configured DB. Do not fallback to sqlite.
database.ConnectDB()
if database.DB == nil {
fmt.Println("Database not configured or connection failed. Please set DB_URL in .env and ensure database is reachable.")
os.Exit(1)
}
// set GORM logger to Silent for seeding to reduce noise
var db *gorm.DB = database.DB.Session(&gorm.Session{Logger: logger.Default.LogMode(logger.Silent)})
fmt.Println("Using configured DB")
// auto-migrate minimal models used
err := db.AutoMigrate(&models.Category{}, &models.Tag{}, &models.Post{}, &models.Comment{})
if err != nil {
fmt.Println("AutoMigrate failed:", err)
os.Exit(1)
}
rand.Seed(time.Now().UnixNano())
// create categories
cats := []models.Category{}
catNames := []string{"Teknoloji", "Yazilim", "Guncel", "Yasam", "Egitim", "Spor", "Saglik", "Finans"}
for _, n := range catNames {
baseSlug := slugify(n)
var c models.Category
if err := db.Where("title = ?", n).First(&c).Error; err == nil {
// Ensure old records have a valid unique slug.
if strings.TrimSpace(c.Slug) == "" {
c.Slug = ensureUniqueCategorySlug(db, baseSlug)
_ = db.Save(&c).Error
}
cats = append(cats, c)
continue
}
c = models.Category{
Title: n,
Slug: ensureUniqueCategorySlug(db, baseSlug),
Description: fmt.Sprintf("%s kategorisi seed verisi", n),
}
res := db.Create(&c)
if res.Error != nil {
fmt.Println("Failed to create category", n, ":", res.Error)
continue
}
cats = append(cats, c)
fmt.Println("Created category:", c.Title, "slug:", c.Slug)
}
// create tags
tags := []models.Tag{}
tagNames := []string{"go", "fiber", "backend", "mysql", "redis", "jwt", "api", "docker", "cloud", "devops", "security", "testing"}
for _, n := range tagNames {
var t models.Tag
if err := db.Where("name = ?", n).First(&t).Error; err == nil {
tags = append(tags, t)
continue
}
t = models.Tag{Name: n}
res := db.Create(&t)
if res.Error != nil {
fmt.Println("Failed to create tag", n, ":", res.Error)
continue
}
tags = append(tags, t)
fmt.Println("Created tag:", t.Name)
}
// create posts
dest := "./uploads/posts"
targetPosts := 39
for i := 1; i <= targetPosts; i++ {
imgPath, err := downloadImage(dest, i)
if err != nil {
fmt.Println("image download failed for", i, "— using fallback path. err:", err)
imgPath = fmt.Sprintf("uploads/posts/fallback_%d.jpg", i)
}
title := fmt.Sprintf("Seed Post %d", i)
baseSlug := slugify(title)
uniqueSlug := ensureUniquePostSlug(db, baseSlug)
p := models.Post{
Title: title,
Slug: uniqueSlug,
Content: fmt.Sprintf("Bu bir test icerigidir. Gonderi numarasi %d.", i),
}
// randomly attach 1-2 categories
nc := rand.Intn(2) + 1
permCats := rand.Perm(len(cats))[:nc]
for _, idx := range permCats {
p.Categories = append(p.Categories, cats[idx])
}
// attach 1-3 tags
nx := rand.Intn(3) + 1
permTags := rand.Perm(len(tags))[:nx]
for _, idx := range permTags {
p.Tags = append(p.Tags, tags[idx])
}
// assign a single relative image path (NOT JSON array)
p.Images = imgPath
res := db.Create(&p)
if res.Error != nil {
fmt.Println("Failed to create post", title, ":", res.Error)
} else {
fmt.Println("Created post", p.Title, "slug:", p.Slug)
}
}
fmt.Printf("Seeding done — %d posts targeted.\n", targetPosts)
// print reminder where images are
fmt.Println("Images saved to ./uploads/posts — check files.")
}

53
services/email_service.go Normal file
View File

@@ -0,0 +1,53 @@
package services
import (
"fmt"
"net/smtp"
"strings"
configs "goFiber/config"
)
type EmailService struct{}
func NewEmailService() *EmailService {
return &EmailService{}
}
func (s *EmailService) Send(to, subject, body string) error {
host := strings.TrimSpace(configs.AppConfig.EmailHost)
port := strings.TrimSpace(configs.AppConfig.EmailPort)
from := strings.TrimSpace(configs.AppConfig.EmailFrom)
if host == "" || port == "" || from == "" {
return fmt.Errorf("email configuration is incomplete")
}
addr := host + ":" + port
username := strings.TrimSpace(configs.AppConfig.EmailHostUser)
password := strings.TrimSpace(configs.AppConfig.EmailHostPassword)
var auth smtp.Auth
if username != "" && password != "" {
auth = smtp.PlainAuth("", username, password, host)
}
message := "From: " + from + "\r\n" +
"To: " + to + "\r\n" +
"Subject: " + subject + "\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n\r\n" +
body
return smtp.SendMail(addr, auth, from, []string{to}, []byte(message))
}
func (s *EmailService) SendVerificationEmail(to, firstName, verifyURL string) error {
subject := "Email verification"
body := fmt.Sprintf(
"Hi %s,\n\nPlease verify your email by opening this link:\n%s\n\nIf you did not create this account, you can ignore this email.",
firstName,
verifyURL,
)
return s.Send(to, subject, body)
}

121
services/jwt_service.go Normal file
View File

@@ -0,0 +1,121 @@
package services
import (
"errors"
"strconv"
"time"
configs "goFiber/config"
"log"
"github.com/golang-jwt/jwt/v5"
)
const (
TokenTypeAccess = "access"
TokenTypeRefresh = "refresh"
)
type JWTClaim struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
IsAdmin bool `json:"is_admin"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
TokenType string `json:"token_type"`
jwt.RegisteredClaims
}
type JWTService struct{}
func NewJWTService() *JWTService {
return &JWTService{}
}
func (s *JWTService) GenerateToken(
userID uint,
email string,
isAdmin bool,
firstName string,
lastName string,
tokenType string,
expiration time.Duration,
) (string, error) {
now := time.Now()
claims := &JWTClaim{
UserID: userID,
Email: email,
IsAdmin: isAdmin,
FirstName: firstName,
LastName: lastName,
TokenType: tokenType,
RegisteredClaims: jwt.RegisteredClaims{
Subject: strconv.FormatUint(uint64(userID), 10),
ExpiresAt: jwt.NewNumericDate(now.Add(expiration)),
IssuedAt: jwt.NewNumericDate(now),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(configs.AppConfig.JWTSecret))
}
func (s *JWTService) GenerateTokenPair(
userID uint,
email string,
isAdmin bool,
firstName string,
lastName string,
) (string, string, error) {
access, err := s.GenerateToken(
userID,
email,
isAdmin,
firstName,
lastName,
TokenTypeAccess,
time.Duration(configs.AppConfig.AccessTokenExpireMinutes)*time.Minute,
)
if err != nil {
return "", "", err
}
refresh, err := s.GenerateToken(
userID,
email,
isAdmin,
firstName,
lastName,
TokenTypeRefresh,
time.Duration(configs.AppConfig.RefreshTokenExpireDays)*24*time.Hour,
)
if err != nil {
return "", "", err
}
// Log generated tokens (access + refresh)
log.Printf("Generated token pair for user=%d email=%s access_exp=%dm refresh_exp=%dd", userID, email, configs.AppConfig.AccessTokenExpireMinutes, configs.AppConfig.RefreshTokenExpireDays)
log.Printf("access: %s", access)
log.Printf("refresh: %s", refresh)
return access, refresh, nil
}
func (s *JWTService) ValidateToken(signedToken string) (*JWTClaim, error) {
token, err := jwt.ParseWithClaims(signedToken, &JWTClaim{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(configs.AppConfig.JWTSecret), nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*JWTClaim)
if !ok || !token.Valid {
return nil, errors.New("invalid token claims")
}
return claims, nil
}

1
swaginit.sh Normal file
View File

@@ -0,0 +1 @@
swag init -g main.go -o docs --parseDependency --parseInternal

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Some files were not shown because too many files have changed in this diff Show More