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 |