first commit
58
.air.toml
Normal 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
@@ -0,0 +1,12 @@
|
||||
.git
|
||||
.idea
|
||||
.vscode
|
||||
tmp
|
||||
uploads
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
main
|
||||
.env
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
65
.env
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
1364
controllers/blog_controller.go
Normal file
210
controllers/hero_controller.go
Normal 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"})
|
||||
}
|
||||
565
controllers/security_controller.go
Normal 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...)
|
||||
}
|
||||
}
|
||||
218
controllers/setting_controller.go
Normal 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
@@ -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
|
||||
}
|
||||
39
database/config/mysql_db.go
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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"`
|
||||
}
|
||||
39
database/models/docs_models.go
Normal 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
@@ -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"`
|
||||
}
|
||||
35
database/models/setting.go
Normal 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
@@ -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
@@ -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
@@ -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
4089
docs/swagger.json
Normal file
2633
docs/swagger.yaml
Normal file
71
go.mod
Normal 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
@@ -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
@@ -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))
|
||||
}
|
||||
63
middlewares/auth_middleware.go
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
swag init -g main.go -o docs --parseDependency --parseInternal
|
||||
|
After Width: | Height: | Size: 246 KiB |
BIN
uploads/avatars/1771193407_1657955547black-google-icon.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
uploads/avatars/1771193710_avatar-1771193710406.avif
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
uploads/avatars/1771193960_avatar-1771193960160.avif
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
uploads/avatars/1771193974_avatar-1771193974126.avif
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
uploads/avatars/1771194297_avatar-1771194297610.avif
Normal file
|
After Width: | Height: | Size: 628 B |
BIN
uploads/heroes/1771180434_img-1771180434213-1574.avif
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
uploads/heroes/1771180752_img-1771180752790-8682.avif
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
uploads/heroes/1771180820_img-1771180820643-8126.avif
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
uploads/heroes/1771180922_img-1771180922370-1617.avif
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
uploads/heroes/1771181547_img-1771181547365-491.avif
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
uploads/heroes/1771181963_img-1771181963369-3463.avif
Normal file
|
After Width: | Height: | Size: 812 B |
BIN
uploads/heroes/1771182081_img-1771182080929-8743.avif
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 28 KiB |
BIN
uploads/posts/1771321021089007000_img-1771321020975.avif
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
uploads/posts/1771323398895282000_img-1771323398885.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
uploads/posts/1771323760968199000_img-1771323760962.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
uploads/posts/1771324096734046000_img-1771324096723.jpg
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
uploads/posts/post_10_19b004.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
uploads/posts/post_11_1447b0.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
uploads/posts/post_12_ddfaa3.jpg
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
uploads/posts/post_13_b37f04.jpg
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
uploads/posts/post_14_c21ae0.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
uploads/posts/post_15_0a1be0.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
uploads/posts/post_16_6a0c58.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
uploads/posts/post_17_2b30e4.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
uploads/posts/post_18_ec88ac.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
uploads/posts/post_19_25715c.jpg
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
uploads/posts/post_1_75e8c4.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
uploads/posts/post_20_09d967.jpg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
uploads/posts/post_21_626d54.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
uploads/posts/post_22_2aa79a.jpg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
uploads/posts/post_23_4ff46d.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
uploads/posts/post_24_97be20.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
uploads/posts/post_25_14eeaf.jpg
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
uploads/posts/post_26_ce05c4.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
uploads/posts/post_27_c75542.jpg
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
uploads/posts/post_28_a10a99.jpg
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
uploads/posts/post_29_5a1d1f.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
uploads/posts/post_2_a15fd6.jpg
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
uploads/posts/post_30_d116c0.jpg
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
uploads/posts/post_31_7a6f61.jpg
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
uploads/posts/post_32_214285.jpg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
uploads/posts/post_33_640f31.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
uploads/posts/post_34_6f7963.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
uploads/posts/post_35_30ee16.jpg
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
uploads/posts/post_36_36c542.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
uploads/posts/post_37_573af9.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
uploads/posts/post_38_4df36e.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
uploads/posts/post_39_0e773c.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
uploads/posts/post_3_291f5f.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
uploads/posts/post_4_3d6f3f.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
uploads/posts/post_5_954662.jpg
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
uploads/posts/post_6_ad3af8.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
uploads/posts/post_7_d973fd.jpg
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
uploads/posts/post_8_80cbb5.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
uploads/posts/post_9_03c3b8.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
uploads/settings/b_1771327728_img-1771327728548-3442.avif
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
uploads/settings/w_1771327728_img-1771327728388-5953.avif
Normal file
|
After Width: | Height: | Size: 7.3 KiB |