first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:30:42 +03:00
commit 4d92991817
1982 changed files with 284835 additions and 0 deletions

58
.air.toml Normal file
View File

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

32
.env Normal file
View File

@@ -0,0 +1,32 @@
### Db Configuration
DB_URL="goares:gg7678290@tcp(10.80.80.70:3306)/goares?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s&readTimeout=30s&writeTimeout=30s&multiStatements=true"
DB_URL_PG=host=10.80.80.70 user=cloud password=gg7678290 dbname=goares port=5432 sslmode=disable TimeZone=Europe/Istanbul
##########################
# 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=ares-fiber-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
################################
CORS_DEBUG=true
VITE_API_BASE_URL=http://localhost:8080
FRONTEND_URL=http://localhost:3000
SESSION_SECRET=go-fiber-mTFY2jAOMWWxadVIWjRoPG9aOM3z9srCVoU35Gs1VZaRKgXet26cztUE8LLpwok9
CLOUD_FLARE_SITE_KEY='0x4AAAAAACHzHKvlEwMamxCM'
CLOUD_FLARE_SECRET='0x4AAAAAACHzHHisTSFzGw15HvwXF3yXRIg'

31
.env.copy Normal file
View File

@@ -0,0 +1,31 @@
### Db Configuration
DB_URL="goares:gg7678290@tcp(10.80.80.70:3306)/goares?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=ares-fiber-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
################################
CORS_DEBUG=true
VITE_API_BASE_URL=http://localhost:8080
FRONTEND_URL=http://localhost:3000
SESSION_SECRET=go-fiber-mTFY2jAOMWWxadVIWjRoPG9aOM3z9srCVoU35Gs1VZaRKgXet26cztUE8LLpwok9
CLOUD_FLARE_SITE_KEY='0x4AAAAAACHzHKvlEwMamxCM'
CLOUD_FLARE_SECRET='0x4AAAAAACHzHHisTSFzGw15HvwXF3yXRIg'

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
### 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
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
tmp
ares
main
.DS_Store
.idea

View File

@@ -0,0 +1,9 @@
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJzdWIiOiI2IiwiZXhwIjoxNzcxNzAyMTU2LCJpYXQiOjE3NzE2OTQ5NTZ9.QHid2xqKsdwe1E-vkrZLA7nB_qL3DEcEWztbkFoOaZU
a1a4e309bb7b1ea86c5c046a22a5d5e4f7ec727a
81f4318fe76b595c582aa9f2baf26818894303d0

71
Dockerfile Normal file
View File

@@ -0,0 +1,71 @@
# Builder: use Debian-based Go image so we can install libvips with AVIF support
FROM golang:1.25.7-bookworm AS builder
# Install build dependencies including libvips dev packages and AVIF/heif libs
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
pkg-config \
gcc \
libc6-dev \
libvips-dev \
libvips-tools \
libheif-dev \
libavif-dev \
libjpeg-dev \
libpng-dev \
libwebp-dev \
ca-certificates \
make \
git \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy the source code
COPY . .
# Build the application (CGO_ENABLED=1 required for bimg)
RUN CGO_ENABLED=1 GOOS=linux go build -o main .
# --- Final Stage ---
FROM debian:bookworm-slim
# Install runtime dependencies: libvips and supporting libs (with AVIF support)
RUN apt-get update && apt-get install -y --no-install-recommends \
libvips \
libheif-dev \
libavif-dev \
ca-certificates \
tzdata \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy the binary and assets from the builder stage
COPY --from=builder /app/main .
COPY --from=builder /app/views ./views
COPY --from=builder /app/public ./public
# Create uploads directory
RUN mkdir -p uploads && chown -R 1000:1000 /app/uploads
# Add entrypoint and make executable
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Create a non-root user for running the app
RUN groupadd -g 1000 appuser && useradd -u 1000 -g appuser -s /bin/sh -m appuser
# Expose port
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
USER appuser
CMD ["./main"]

96
belgeler/admin_panel.md Normal file
View File

@@ -0,0 +1,96 @@
You are an AI developer assistant. Your task is to generate a complete admin panel scaffold (HTML templates, static assets and server-side route stubs) for a Go Fiber project. Use only the following frontend libraries already available in the project dependencies: Alpine.js, Bootstrap 5, HTMX, jQuery. Do not add other frameworks. Produce code artifacts described in the "Outputs" section below.
Constraints & requirements (önemli)
- Kullanılan frontend kütüphaneleri: Alpine.js (v3), HTMX, Bootstrap 5, jQuery — bunları proje /public üzerinden kullan.
- Tema: Koyu ve aydın (dark/light) destekli olacak. Kullanıcı temayı değiştirdiğinde tercih localStorage'da saklansın ve sayfa yenilendiğinde korunmalı.
- Layout: Sol sabit (desktop) sidebar; mobilde off-canvas veya slide-in davranışı. Üstte sticky header (navbar) — hep görünür. Ana içerik HTMX ile parçalar halinde yüklensin (partials/fragments).
- Login sayfası: Cloudflare Turnstile ile korumalı. Turnstile için sitekey ve secret key placeholder kullanılacak (örneğin: YOUR_TURNSTILE_SITEKEY, YOUR_TURNSTILE_SECRET). Login formu HTMX ile POST gönderecek, sunucu Turnstile doğrulaması yapacak.
- Accessibility: ARIA nitelikleri eklensin, klavye navigasyonu çalışsın, renk kontrastı erişilebilir seviyede olsun.
- Responsive: Desktop / tablet / mobile uyumlu olmalı.
- Minimal JS: Alpine.js ile UI state yönetimi (tema toggle, mobil sidebar), HTMX ile partial yükleme, küçük helper JS dosyası için jQuery veya vanilla kullanılabilir. Büyük bundler veya build pipeline gerektirmeyecek.
- Security: CSRF, XSS ve session güvenliği için sunucu tarafı notları verilecek (CSRF token uygulaması önerisi, Turnstile server-side doğrulama, secure cookie flags, rate limiting).
- Dosya/dizin yapısı: templates/ (HTML şablonları), public/css, public/js, public/assets. Server tarafı Go Fiber rotaları ve basit handler açıklamaları verilecek (kod değil; hangi endpoint ne döner).
Görsel / UX istekleri
- Tema değişimi için hem simge hem metin olacak (ör. güneş/ay). Geçiş animasyonu küçük (0.2s - 0.35s).
- Sidebar: ikon + etiket, aktif öğe vurgulu, mümkünse collapse/expand (yalnızca genişlik daraltıldığında).
- Header: sol marka, ortada opsiyonel breadcrumb (isteğe bağlı), sağda tema toggle ve kullanıcı dropdown.
- İçerik alanı: kart bazlı, boşluklar (padding/margin) dengeli.
- Animasyonlar: HTMX swap sırasında fade veya slide geçişi (kısa).
- Renk paleti: net iki palet tanımı (aydın için açık nötr arka plan + mavi aksan; koyu için koyu mavi/şeftali aksan). Renkler CSS custom properties (CSS değişkenleri) ile tanımlansın.
Girdi (input)
- ./public Alpine.js, Bootstrap, HTMX, jQuery (mevcut).
- Kullanıcı sağlayacak: Cloudflare Turnstile sitekey/secret (placeholder bırakılacak).
- Sunucu: Go Fiber (kullanıcı Fiber projesine kolayca entegre edebilsin).
Outputs — AI'nin üretmesi gerekenler (tam liste)
1. Temel layout şablonu (templates/layout.html veya .tmpl):
- ./public/assets (Bootstrap CSS/JS, HTMX, Alpine, jQuery, Turnstile script).
- Body: sticky header, sol sidebar, main content container (#main-content) — HTMX hedefi.
- Tema yönetimi için Alpine veri nesnesi referansları (tema toggle, mobile sidebar kontrol).
- Ana layout'ta HTMX ile fragment yüklemeye uygun linkler (hx-get, hx-target="#main-content", hx-swap).
2. Login şablonu (templates/login.html):
- Cloudflare Turnstile widget (data-sitekey placeholder).
- Form HTMX ile POST atacak (hx-post="/login", hx-target="#login-feedback" vb.).
- Başarı/hata fragment'leri için sunucu tarafı cevap formatı belirtilecek.
3. Örnek HTMX fragment'leri (templates/fragments/*):
- dashboard fragment
- users list fragment
- settings fragment
(her fragment minimal içerik, HTMX swap ile çalışacak biçimde)
4. public/js/main.js (küçük açıklama, içerik oluşturulacak):
- Alpine tema manager yapısı (isDark reactive, save/load localStorage, toggle fonksiyonları).
- Mobile sidebar toggle fonksiyonları.
- HTMX üzerinden fragment yüklenirken küçük loading indicator veya class ekleme.
- Turnstile ile integration notu (widget render ve token gönderimi; token'ın login formu ile nasıl dahil olacağı).
5. public/css/theme.css:
- CSS custom properties hem light hem dark için.
- Sidebar, header, card stilleri, responsive medya sorguları.
- Küçük animasyonlar (fade, transition).
6. Sunucu tarafı: Go Fiber rotaları listesi ve davranış açıklamaları (kod değil, endpoint + beklenen request/response):
- GET /login -> login şablonunu döndür
- POST /login -> Turnstile token doğrulaması + credentials doğrulama; başarılıysa session cookie oluştur ve /admin yönlendir (HTMX için fragment veya redirect)
- GET /admin -> layout render (veya layout + default fragment)
- GET /admin/content/dashboard -> dashboard fragment (HTMX hedefi)
- GET /admin/content/users -> users fragment
- GET /admin/content/settings -> settings fragment
- GET /logout -> oturumu sonlandır
- admin middleware ye agit tum rotalar
- (Opsiyonel) POST /api/users/* gibi API uç noktaları (JSON) — HTMX yerine AJAX gerekeceğinde kullanılacak
7. Güvenlik & operasyonel notlar (dokümantasyon/metin):
- Turnstile: server-side doğrulama nasıl yapılır (istek: secret ve token ile Cloudflare API endpoint çağrısı; doğrulama şartı).
- CSRF: HTMX formlarında CSRF token nasıl geçilir (hidden input veya header); Fiber için örnek header adı ve cookie ilişkisi (kod değil açıklama).
- Session yönetimi: secure, httpOnly, SameSite=strict/ Lax önerisi.
- Rate limiting önerisi (ör. login için).
- Input validation and output escaping reminder.
8. Acceptance criteria / test senaryoları:
- Tema toggle yapıldığında localStorage güncellenmeli ve sayfa yenilendiğinde tercih korunmalı.
- Sidebar linkine tıklandığında HTMX ile fragment yüklensin, header sabit kalsın.
- Mobilde sidebar toggle düzgün çalışsın ve overlay kapanışı sağlansın.
- Login formu Turnstile widget üretiyor; form gönderildiğinde Sunucu Turnstile token'ı doğruluyor — geçerse oturum açma gerçekleşiyor, geçmezse hata mesajı.
- Erişilemeyen bir fragment istenirse HTMX hedefinde uygun hata gösterimi (alert/flash) gelsin.
- Temel a11y kontrolü: tüm interaktif öğeler klavye ile erişilebilir olsun, img/ikonlar için alt metin/aria-label.
Ek istekler / tercih edilen çıktı biçimi
- Üretilen dosyaları dosya isimleriyle ve kısa açıklamalarıyla listelerken KOD VERMEYİN — sadece hangi dosyayı ve ne içermesi gerektiğini net maddelemeyle verin (ama AI kod da üretirse ayrı bir adımda bunu isteyebilirim).
- Turnstile sitekey/secret için placeholder kullanın ve kullanıcıya nerede değiştireceğini açıkça belirtin.
- HTMX fragment response biçimi: tam HTML fragment (sadece main-content içeriği) veya JSON+HTML toggles; tercihen saf HTML fragment kullanılsın.
- Her dosya için kısa "örnek içerik açıklaması" verin (ör. dashboard fragment: 3 adet statistic card ve bir tablo).
Çıktı teslim formatı
- Lütfen üretim çıktısını şu şekilde organize et:
- "Files to create" listesi (path + kısa açıklama)
- "Server endpoints" listesi (method + path + beklenen davranış)
- "Güvenlik & deployment notları"
- "Acceptance tests"
- Tekrar ediyorum: BU PROMPT sadece kılavuzdur — şu an yalnızca prompt dosyasını istiyorum, KOD YOK. Eğer kod oluşturmaya hazırsan, ayrı bir istekte bulunacağım.
Not: Kullanıcı adı ve mevcut package.json bağımlılıklarını unutma: Alpine.js, Bootstrap 5, htmx, jquery. Cloudflare Turnstile sitekey/secret işaretlerini placeholder bırak.

119
config/config.go Normal file
View File

@@ -0,0 +1,119 @@
package configs
import (
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
"go.uber.org/zap"
)
type Config struct {
Env string // örn. development, production
Port string
DBUrl string
DBPGUrl 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
// 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 {
// Logger henüz hazır olmayabilir; varsa kullan
if Logger != nil {
Logger.Warn("Uyarı: .env dosyası yüklenirken hata oluştu, sistem ortam değişkenleriyle devam ediliyor", zapFieldsForEnvError(err)...)
}
}
AppConfig = &Config{
Env: getEnv("APP_ENV", "development"),
Port: getEnv("PORT", "8080"),
DBUrl: getEnv("DB_URL", ""),
DBPGUrl: getEnv("DB_URL_PG", ""),
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
// 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
}
func zapFieldsForEnvError(err error) []zap.Field {
return []zap.Field{
zap.String("error", err.Error()),
}
}

39
config/loger.go Normal file
View File

@@ -0,0 +1,39 @@
package configs
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Logger *zap.Logger
func init() {
// initialize default logger early so other packages can use it in their init functions
LogerAres()
}
func LogerAres() {
logFile, err := os.Create("./info.log")
if err != nil {
// Fallback: sadece konsola yaz
core := zapcore.NewCore(
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
zapcore.AddSync(os.Stdout),
zapcore.DebugLevel,
)
Logger = zap.New(core, zap.AddCaller())
Logger.Warn("info.log açılamadı, sadece konsola yazılıyor", zap.Error(err))
return
}
// Hem dosyaya hem konsola yaz (logları görebilirsin)
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoder := zapcore.NewConsoleEncoder(encoderConfig)
multiOut := zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(logFile))
core := zapcore.NewCore(encoder, multiOut, zapcore.DebugLevel)
Logger = zap.New(core, zap.AddCaller())
Logger.Info("Logger başlatıldı (konsol + info.log)")
}

View File

@@ -0,0 +1,52 @@
package controllers
import (
dbConfig "ares/database/config"
"ares/database/models"
"strconv"
"github.com/gofiber/fiber/v3"
)
// AdminContentCarts handles rendering the Carts list in the admin panel
func AdminContentCarts(c fiber.Ctx) error {
var carts []models.Cart
// Preload the User to display who owns the cart
// Preload Items to show the item count
query := dbConfig.DB.Model(&models.Cart{}).Preload("Items")
// Filter by User ID if a search is provided (basic example, finding user by ID)
search := c.Query("search")
if search != "" {
if userID, err := strconv.Atoi(search); err == nil {
query = query.Where("user_id = ?", userID)
}
}
query.Order("updated_at desc").Find(&carts)
data := fiber.Map{
"Carts": carts,
"Search": search,
}
// Render partial if requested via HTMX
if c.Get("HX-Request") == "true" {
return c.Render("admin/partials/carts", data)
}
// Otherwise render full layout
return c.Render("admin/partials/carts", data, "admin/layout")
}
// AdminCartDelete handles deleting a cart (useful for clearing abandoned carts)
func AdminCartDelete(c fiber.Ctx) error {
id := c.Params("id")
if err := dbConfig.DB.Unscoped().Where("id = ?", id).Delete(&models.Cart{}).Error; err != nil {
return c.Redirect().To("/admin/content/carts?error=Silme+başarısız")
}
return c.Redirect().To("/admin/content/carts?deleted=true&success=Sepet+silindi")
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,619 @@
package controllers
import (
dbConfig "ares/database/config"
"ares/database/models"
utils "ares/pkg/utis"
"ares/services"
"math"
"strconv"
"github.com/gofiber/fiber/v3"
)
// AdminContentProducts renders the products partial
func AdminContentProducts(c fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
limit := 20
offset := (page - 1) * limit
search := c.Query("search", "")
showDeleted := c.Query("deleted") == "true"
var products []models.Product
var total int64
query := dbConfig.DB.Model(&models.Product{})
if showDeleted {
query = query.Unscoped().Where("deleted_at IS NOT NULL")
}
if search != "" {
query = query.Where("title LIKE ? OR slug LIKE ?", "%"+search+"%", "%"+search+"%")
}
query.Count(&total)
query.Preload("Categories").Preload("Tags").Order("created_at desc").Limit(limit).Offset(offset).Find(&products)
imageMap := make(map[uint]string)
for _, p := range products {
if p.Images != "" {
imgs := parseImagesField(p.Images)
if len(imgs) > 0 {
imageMap[p.ID] = imgs[0]
}
}
}
totalPages := int(math.Ceil(float64(total) / float64(limit)))
data := fiber.Map{
"Products": products,
"ImageMap": imageMap,
"Page": page,
"TotalPages": totalPages,
"NextPage": page + 1,
"PrevPage": page - 1,
"Search": search,
"ShowDeleted": showDeleted,
}
if c.Get("HX-Request") == "true" {
return c.Render("admin/partials/products", data)
}
return c.Render("admin/partials/products", data, "admin/layout")
}
// AdminProductNew renders create form
func AdminProductNew(c fiber.Ctx) error {
var cats []models.ProductCategory
var tags []models.ProductTag
dbConfig.DB.Order("title asc").Find(&cats)
dbConfig.DB.Order("name asc").Find(&tags)
return c.Render("admin/pages/product_form", fiber.Map{"IsEdit": false, "Categories": cats, "Tags": tags, "FirstImage": ""}, "admin/layout")
}
// AdminProductCreate handles creation
func AdminProductCreate(c fiber.Ctx) error {
title := c.FormValue("title")
if title == "" {
return c.Redirect().To("/admin/content/products?error=Başlık+gerekli")
}
product := models.Product{Title: title}
product.Content = c.FormValue("content")
// Slug handling
rawSlug := c.FormValue("slug")
if rawSlug == "" {
rawSlug = utils.Slugify(product.Title)
} else {
rawSlug = utils.Slugify(rawSlug)
}
attempt := rawSlug
i := 1
for {
var existing models.Product
err := dbConfig.DB.Unscoped().Where("slug = ?", attempt).First(&existing).Error
if err != nil {
break
}
attempt = rawSlug + "-" + strconv.Itoa(i)
i++
}
product.Slug = attempt
// Image Upload
priceStr := c.FormValue("price")
price, _ := strconv.ParseFloat(priceStr, 64)
width, _ := strconv.Atoi(c.FormValue("width"))
height, _ := strconv.Atoi(c.FormValue("height"))
quality, _ := strconv.Atoi(c.FormValue("quality"))
format := c.FormValue("format")
if format == "" {
format = "avif"
}
// DB fields
product.Price = price
product.Format = format
product.Width = width
product.Height = height
product.Quality = quality
// Use processAndSaveImage for main product image (for simplicity we store 1 image mapped as JSON array or just single string per parseImagesField)
imagePath, err := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
Width: width,
Height: height,
Quality: quality,
Format: format,
Folder: "products",
})
if err == nil && imagePath != "" {
// Tek resim olarak string kaydet
product.Images = imagePath
}
if err := dbConfig.DB.Create(&product).Error; err != nil {
return c.Redirect().To("/admin/content/products?error=Oluşturma+başarısız")
}
// Handline Relations (Categories & Tags)
form, err := c.MultipartForm()
if err == nil && form != nil {
catIDs := form.Value["categories"]
for _, catIDStr := range catIDs {
catID, _ := strconv.Atoi(catIDStr)
if catID > 0 {
var cat models.ProductCategory
if err := dbConfig.DB.First(&cat, catID).Error; err == nil {
dbConfig.DB.Model(&product).Association("Categories").Append(&cat)
}
}
}
tagIDs := form.Value["tags"]
for _, tagIDStr := range tagIDs {
tagID, _ := strconv.Atoi(tagIDStr)
if tagID > 0 {
var tag models.ProductTag
if err := dbConfig.DB.First(&tag, tagID).Error; err == nil {
dbConfig.DB.Model(&product).Association("Tags").Append(&tag)
}
}
}
} else {
// if form data is not multipart, try basic FormValue array
// Though Fiber usually parses multiple values when we get them explicitly or we can just parse the body
}
return c.Redirect().To("/admin/content/products?success=Ürün+oluşturuldu")
}
// AdminProductEdit renders the edit hero form
func AdminProductEdit(c fiber.Ctx) error {
id := c.Params("id")
var product models.Product
if err := dbConfig.DB.Preload("Categories").Preload("Tags").First(&product, id).Error; err != nil {
return c.Status(fiber.StatusNotFound).SendString("Ürün bulunamadı")
}
var cats []models.ProductCategory
var tags []models.ProductTag
dbConfig.DB.Order("title asc").Find(&cats)
dbConfig.DB.Order("name asc").Find(&tags)
firstImage := ""
if product.Images != "" {
imgs := parseImagesField(product.Images)
if len(imgs) > 0 {
firstImage = imgs[0]
}
}
return c.Render("admin/pages/product_form", fiber.Map{
"IsEdit": true,
"Product": product,
"Categories": cats,
"Tags": tags,
"FirstImage": firstImage,
}, "admin/layout")
}
// AdminProductUpdate handles product update
func AdminProductUpdate(c fiber.Ctx) error {
id := c.Params("id")
var product models.Product
if err := dbConfig.DB.First(&product, id).Error; err != nil {
return c.Redirect().To("/admin/content/products?error=Ürün+bulunamadı")
}
product.Title = c.FormValue("title")
product.Content = c.FormValue("content")
// Slug update
rawSlug := c.FormValue("slug")
if rawSlug != "" && rawSlug != product.Slug {
rawSlug = utils.Slugify(rawSlug)
attempt := rawSlug
i := 1
for {
var existing models.Product
err := dbConfig.DB.Unscoped().Where("slug = ? AND id != ?", attempt, product.ID).First(&existing).Error
if err != nil {
break
}
attempt = rawSlug + "-" + strconv.Itoa(i)
i++
}
product.Slug = attempt
}
// Image Upload
priceStr := c.FormValue("price")
price, _ := strconv.ParseFloat(priceStr, 64)
width, _ := strconv.Atoi(c.FormValue("width"))
height, _ := strconv.Atoi(c.FormValue("height"))
quality, _ := strconv.Atoi(c.FormValue("quality"))
format := c.FormValue("format")
if format == "" {
format = "avif"
}
// DB fields
product.Price = price
product.Format = format
product.Width = width
product.Height = height
product.Quality = quality
imagePath, err := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
Width: width,
Height: height,
Quality: quality,
Format: format,
Folder: "products",
})
if err == nil && imagePath != "" {
product.Images = imagePath
}
if err := dbConfig.DB.Save(&product).Error; err != nil {
return c.Redirect().To("/admin/content/products?error=Güncelleme+başarısız")
}
// Handline Relations (Categories & Tags)
dbConfig.DB.Model(&product).Association("Categories").Clear()
dbConfig.DB.Model(&product).Association("Tags").Clear()
form, err := c.MultipartForm()
if err == nil && form != nil {
catIDs := form.Value["categories"]
for _, catIDStr := range catIDs {
catID, _ := strconv.Atoi(catIDStr)
if catID > 0 {
var cat models.ProductCategory
if err := dbConfig.DB.First(&cat, catID).Error; err == nil {
dbConfig.DB.Model(&product).Association("Categories").Append(&cat)
}
}
}
tagIDs := form.Value["tags"]
for _, tagIDStr := range tagIDs {
tagID, _ := strconv.Atoi(tagIDStr)
if tagID > 0 {
var tag models.ProductTag
if err := dbConfig.DB.First(&tag, tagID).Error; err == nil {
dbConfig.DB.Model(&product).Association("Tags").Append(&tag)
}
}
}
}
return c.Redirect().To("/admin/content/products?success=Ürün+güncellendi")
}
// AdminProductDelete handles soft delete
func AdminProductDelete(c fiber.Ctx) error {
id := c.Params("id")
if err := dbConfig.DB.Delete(&models.Product{}, id).Error; err != nil {
return c.Redirect().To("/admin/content/products?error=Silme+hatası")
}
return c.Redirect().To("/admin/content/products?success=Ürün+silindi")
}
// AdminProductRestore handles restore
func AdminProductRestore(c fiber.Ctx) error {
id := c.Params("id")
if err := dbConfig.DB.Unscoped().Model(&models.Product{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
return c.Redirect().To("/admin/content/products?error=Geri+yükleme+hatası")
}
return c.Redirect().To("/admin/content/products?deleted=true&success=Ürün+geri+yüklendi")
}
// --- ProductCategory Management ---
func AdminContentProductCategories(c fiber.Ctx) error {
var categories []models.ProductCategory
showDeleted := c.Query("deleted") == "true"
query := dbConfig.DB.Model(&models.ProductCategory{})
if showDeleted {
query = query.Unscoped().Where("deleted_at IS NOT NULL")
}
query.Preload("Parent").Order("title asc").Find(&categories)
data := fiber.Map{
"Categories": categories,
"ShowDeleted": showDeleted,
}
if c.Get("HX-Request") == "true" {
return c.Render("admin/partials/product_categories", data)
}
return c.Render("admin/partials/product_categories", data, "admin/layout")
}
func AdminProductCategoryNew(c fiber.Ctx) error {
var parents []models.ProductCategory
dbConfig.DB.Order("title asc").Find(&parents)
return c.Render("admin/pages/product_category_form", fiber.Map{
"IsEdit": false,
"Parents": parents,
}, "admin/layout")
}
func AdminProductCategoryCreate(c fiber.Ctx) error {
cat := new(models.ProductCategory)
if err := c.Bind().Body(cat); err != nil {
return c.Redirect().To("/admin/content/product-categories?error=Geçersiz+istek")
}
if cat.Title == "" {
return c.Redirect().To("/admin/content/product-categories?error=Başlık+zorunlu")
}
rawSlug := c.FormValue("slug")
if rawSlug == "" {
rawSlug = utils.Slugify(cat.Title)
} else {
rawSlug = utils.Slugify(rawSlug)
}
attempt := rawSlug
i := 1
for {
var existing models.ProductCategory
err := dbConfig.DB.Unscoped().Where("slug = ?", attempt).First(&existing).Error
if err != nil {
break
}
attempt = rawSlug + "-" + strconv.Itoa(i)
i++
}
cat.Slug = attempt
// Handle ParentID
parentIDStr := c.FormValue("parent_id")
if parentIDStr != "" {
if pid, err := strconv.ParseUint(parentIDStr, 10, 32); err == nil && pid > 0 {
pidUint := uint(pid)
cat.ParentID = &pidUint
}
} else {
cat.ParentID = nil
}
if err := dbConfig.DB.Create(cat).Error; err != nil {
return c.Redirect().To("/admin/content/product-categories?error=Oluşturma+başarısız")
}
return c.Redirect().To("/admin/content/product-categories?success=Kategori+oluşturuldu")
}
func AdminProductCategoryEdit(c fiber.Ctx) error {
id := c.Params("id")
var cat models.ProductCategory
if err := dbConfig.DB.First(&cat, id).Error; err != nil {
return c.Redirect().To("/admin/content/product-categories?error=Kategori+bulunamadı")
}
var parents []models.ProductCategory
dbConfig.DB.Where("id != ?", id).Order("title asc").Find(&parents)
return c.Render("admin/pages/product_category_form", fiber.Map{
"IsEdit": true,
"Category": cat,
"Parents": parents,
}, "admin/layout")
}
func AdminProductCategoryUpdate(c fiber.Ctx) error {
id := c.Params("id")
var cat models.ProductCategory
if err := dbConfig.DB.First(&cat, id).Error; err != nil {
return c.Redirect().To("/admin/content/product-categories?error=Kategori+bulunamadı")
}
cat.Title = c.FormValue("title")
cat.Description = c.FormValue("description")
cat.Keywords = c.FormValue("keywords")
rawSlug := c.FormValue("slug")
if rawSlug != "" && rawSlug != cat.Slug {
rawSlug = utils.Slugify(rawSlug)
attempt := rawSlug
i := 1
for {
var existing models.ProductCategory
err := dbConfig.DB.Unscoped().Where("slug = ? AND id != ?", attempt, cat.ID).First(&existing).Error
if err != nil {
break
}
attempt = rawSlug + "-" + strconv.Itoa(i)
i++
}
cat.Slug = attempt
}
parentIDStr := c.FormValue("parent_id")
if parentIDStr != "" {
if pid, err := strconv.ParseUint(parentIDStr, 10, 32); err == nil && pid > 0 {
pidUint := uint(pid)
if pidUint != cat.ID {
cat.ParentID = &pidUint
}
}
} else {
cat.ParentID = nil
}
if err := dbConfig.DB.Save(&cat).Error; err != nil {
return c.Redirect().To("/admin/content/product-categories?error=Güncelleme+başarısız")
}
return c.Redirect().To("/admin/content/product-categories?success=Kategori+güncellendi")
}
func AdminProductCategoryDelete(c fiber.Ctx) error {
id := c.Params("id")
if err := dbConfig.DB.Delete(&models.ProductCategory{}, id).Error; err != nil {
return c.Redirect().To("/admin/content/product-categories?error=Silme+başarısız")
}
return c.Redirect().To("/admin/content/product-categories?success=Kategori+silindi")
}
func AdminProductCategoryRestore(c fiber.Ctx) error {
id := c.Params("id")
if err := dbConfig.DB.Unscoped().Model(&models.ProductCategory{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
return c.Redirect().To("/admin/content/product-categories?error=Geri+yükleme+başarısız")
}
return c.Redirect().To("/admin/content/product-categories?deleted=true&success=Kategori+geri+yüklendi")
}
// --- ProductTag Management ---
func AdminContentProductTags(c fiber.Ctx) error {
var tags []models.ProductTag
showDeleted := c.Query("deleted") == "true"
query := dbConfig.DB.Model(&models.ProductTag{})
if showDeleted {
query = query.Unscoped().Where("deleted_at IS NOT NULL")
}
query.Order("name asc").Find(&tags)
data := fiber.Map{
"Tags": tags,
"ShowDeleted": showDeleted,
}
if c.Get("HX-Request") == "true" {
return c.Render("admin/partials/product_tags", data)
}
return c.Render("admin/partials/product_tags", data, "admin/layout")
}
func AdminProductTagNew(c fiber.Ctx) error {
return c.Render("admin/pages/product_tag_form", fiber.Map{
"IsEdit": false,
}, "admin/layout")
}
func AdminProductTagCreate(c fiber.Ctx) error {
tag := new(models.ProductTag)
if err := c.Bind().Body(tag); err != nil {
return c.Redirect().To("/admin/content/product-tags?error=Geçersiz+istek")
}
if tag.Name == "" {
return c.Redirect().To("/admin/content/product-tags?error=İsim+zorunlu")
}
if err := dbConfig.DB.Create(tag).Error; err != nil {
return c.Redirect().To("/admin/content/product-tags?error=Oluşturma+başarısız")
}
return c.Redirect().To("/admin/content/product-tags?success=Etiket+oluşturuldu")
}
func AdminProductTagEdit(c fiber.Ctx) error {
id := c.Params("id")
var tag models.ProductTag
if err := dbConfig.DB.First(&tag, id).Error; err != nil {
return c.Redirect().To("/admin/content/product-tags?error=Etiket+bulunamadı")
}
return c.Render("admin/pages/product_tag_form", fiber.Map{
"IsEdit": true,
"Tag": tag,
}, "admin/layout")
}
func AdminProductTagUpdate(c fiber.Ctx) error {
id := c.Params("id")
var tag models.ProductTag
if err := dbConfig.DB.First(&tag, id).Error; err != nil {
return c.Redirect().To("/admin/content/product-tags?error=Etiket+bulunamadı")
}
tag.Name = c.FormValue("name")
if err := dbConfig.DB.Save(&tag).Error; err != nil {
return c.Redirect().To("/admin/content/product-tags?error=Güncelleme+başarısız")
}
return c.Redirect().To("/admin/content/product-tags?success=Etiket+güncellendi")
}
func AdminProductTagDelete(c fiber.Ctx) error {
id := c.Params("id")
if err := dbConfig.DB.Delete(&models.ProductTag{}, id).Error; err != nil {
return c.Redirect().To("/admin/content/product-tags?error=Silme+başarısız")
}
return c.Redirect().To("/admin/content/product-tags?success=Etiket+silindi")
}
func AdminProductTagRestore(c fiber.Ctx) error {
id := c.Params("id")
if err := dbConfig.DB.Unscoped().Model(&models.ProductTag{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
return c.Redirect().To("/admin/content/product-tags?error=Geri+yükleme+başarısız")
}
return c.Redirect().To("/admin/content/product-tags?deleted=true&success=Etiket+geri+yüklendi")
}
// AdminContentProductComments handles rendering the Product Comments list in the admin panel
func AdminContentProductComments(c fiber.Ctx) error {
var comments []models.ProductComment
// Preload the User and Product for display (Wait, user and product relationships are missing in model temporarily)
// We'll just list them out manually
query := dbConfig.DB.Model(&models.ProductComment{})
// Optional filtering by specific product via query string
productID := c.Query("product_id")
if productID != "" {
if pid, err := strconv.Atoi(productID); err == nil {
query = query.Where("product_id = ?", pid)
}
}
query.Order("created_at desc").Find(&comments)
data := fiber.Map{
"ProductComments": comments,
"ProductID": productID,
}
if c.Get("HX-Request") == "true" {
return c.Render("admin/partials/product_comments", data)
}
return c.Render("admin/partials/product_comments", data, "admin/layout")
}
// AdminProductCommentDelete handles hard deleting a product comment
func AdminProductCommentDelete(c fiber.Ctx) error {
id := c.Params("id")
if err := dbConfig.DB.Unscoped().Where("id = ?", id).Delete(&models.ProductComment{}).Error; err != nil {
return c.Redirect().To("/admin/content/product-comments?error=Yorum+silinemedi")
}
return c.Redirect().To("/admin/content/product-comments?deleted=true&success=Yorum+silindi")
}
// AdminContentProductCategoryViews handles rendering the Product Category Views list
func AdminContentProductCategoryViews(c fiber.Ctx) error {
var views []models.ProductCategoryView
query := dbConfig.DB.Model(&models.ProductCategoryView{})
// Filter by Category ID
categoryID := c.Query("category_id")
if categoryID != "" {
if cid, err := strconv.Atoi(categoryID); err == nil {
query = query.Where("category_id = ?", cid)
}
}
query.Order("created_at desc").Find(&views)
data := fiber.Map{
"Views": views,
"CategoryID": categoryID,
}
if c.Get("HX-Request") == "true" {
return c.Render("admin/partials/product_category_views", data)
}
return c.Render("admin/partials/product_category_views", data, "admin/layout")
}

View File

@@ -0,0 +1,237 @@
package controllers
import (
database "ares/database/config"
"ares/database/models"
"errors"
"net/http"
"strconv"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
)
// getOrCreateCart is a helper to fetch the cart of the current user.
func getOrCreateCart(userID uint) (models.Cart, error) {
var cart models.Cart
if err := database.DB.Preload("Items").Preload("Items.Product").Where("user_id = ?", userID).First(&cart).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
cart = models.Cart{UserID: userID}
if createErr := database.DB.Create(&cart).Error; createErr != nil {
return cart, createErr
}
return cart, nil
}
return cart, err
}
return cart, nil
}
// GetMyCart godoc
// @Summary Get the current user's cart
// @Tags Cart
// @Produce json
// @Security BearerAuth
// @Success 200 {object} models.CartDoc
// @Failure 401 {object} map[string]string
// @Router /api/v1/cart [get]
func GetMyCart(c fiber.Ctx) error {
userID, ok := c.Locals("user_id").(uint)
if !ok || userID == 0 {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
cart, err := getOrCreateCart(userID)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not load cart"})
}
return c.JSON(cart)
}
// AddToCartRequest represents the body for adding to a cart
type AddToCartRequest struct {
ProductID uint `json:"product_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
}
// AddToCart godoc
// @Summary Add item to cart
// @Tags Cart
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param data body AddToCartRequest true "Cart Item Details"
// @Success 200 {object} models.CartDoc
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/cart/items [post]
func AddToCart(c fiber.Ctx) error {
userID, ok := c.Locals("user_id").(uint)
if !ok || userID == 0 {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
var input AddToCartRequest
if err := c.Bind().Body(&input); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
}
if err := validate.Struct(input); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
// Check product exists
var product models.Product
if err := database.DB.First(&product, input.ProductID).Error; err != nil {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "product not found"})
}
cart, err := getOrCreateCart(userID)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not load cart"})
}
// Check if item already exists in cart
var existingItem models.CartItem
if err := database.DB.Where("cart_id = ? AND product_id = ?", cart.ID, input.ProductID).First(&existingItem).Error; err == nil {
// Update quantity
existingItem.Quantity += input.Quantity
database.DB.Save(&existingItem)
} else {
// Create new item
newItem := models.CartItem{
CartID: cart.ID,
ProductID: input.ProductID,
Quantity: input.Quantity,
}
database.DB.Create(&newItem)
}
// Return updated cart
cart, _ = getOrCreateCart(userID)
return c.JSON(cart)
}
// UpdateCartItemRequest represents the body for updating a cart item quantity
type UpdateCartItemRequest struct {
Quantity int `json:"quantity" validate:"required,min=1"`
}
// UpdateCartItem godoc
// @Summary Update cart item quantity
// @Tags Cart
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param item_id path int true "Cart Item ID"
// @Param data body UpdateCartItemRequest true "Update Quantity"
// @Success 200 {object} models.CartDoc
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/cart/items/{item_id} [put]
func UpdateCartItem(c fiber.Ctx) error {
userID, ok := c.Locals("user_id").(uint)
if !ok || userID == 0 {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
itemID, err := strconv.ParseUint(c.Params("item_id"), 10, 32)
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid item id"})
}
var input UpdateCartItemRequest
if err := c.Bind().Body(&input); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
}
if err := validate.Struct(input); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
cart, err := getOrCreateCart(userID)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not load cart"})
}
var cartItem models.CartItem
if err := database.DB.Where("id = ? AND cart_id = ?", itemID, cart.ID).First(&cartItem).Error; err != nil {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "item not found in your cart"})
}
cartItem.Quantity = input.Quantity
database.DB.Save(&cartItem)
// Return updated cart
cart, _ = getOrCreateCart(userID)
return c.JSON(cart)
}
// RemoveFromCart godoc
// @Summary Remove item from cart
// @Tags Cart
// @Produce json
// @Security BearerAuth
// @Param item_id path int true "Cart Item ID"
// @Success 200 {object} models.CartDoc
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/cart/items/{item_id} [delete]
func RemoveFromCart(c fiber.Ctx) error {
userID, ok := c.Locals("user_id").(uint)
if !ok || userID == 0 {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
itemID, err := strconv.ParseUint(c.Params("item_id"), 10, 32)
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid item id"})
}
cart, err := getOrCreateCart(userID)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not load cart"})
}
var cartItem models.CartItem
if err := database.DB.Where("id = ? AND cart_id = ?", itemID, cart.ID).First(&cartItem).Error; err != nil {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "item not found in your cart"})
}
database.DB.Delete(&cartItem)
// Return updated cart
cart, _ = getOrCreateCart(userID)
return c.JSON(cart)
}
// ClearCart godoc
// @Summary Clear the entire cart
// @Tags Cart
// @Produce json
// @Security BearerAuth
// @Success 200 {object} models.CartDoc
// @Failure 401 {object} map[string]string
// @Router /api/v1/cart [delete]
func ClearCart(c fiber.Ctx) error {
userID, ok := c.Locals("user_id").(uint)
if !ok || userID == 0 {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
cart, err := getOrCreateCart(userID)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not load cart"})
}
// Delete all items for this cart
database.DB.Where("cart_id = ?", cart.ID).Delete(&models.CartItem{})
// Return updated empty cart
cart, _ = getOrCreateCart(userID)
return c.JSON(cart)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,164 @@
package controllers
import (
configs "ares/config"
database "ares/database/config"
"ares/database/models"
"fmt"
"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)
}
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)
}
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 {
configs.Logger.Sugar().Warnf("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)
}
func DeleteHero(c fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
}
var hero models.Hero
if err := database.DB.First(&hero, id).Error; err != nil {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "hero not found"})
}
if err := database.DB.Delete(&hero).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "hero could not be deleted"})
}
return c.JSON(fiber.Map{"message": "hero deleted successfully"})
}

View File

@@ -0,0 +1,161 @@
package controllers
import (
database "ares/database/config"
"ares/database/models"
"errors"
"net/http"
"strconv"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
)
// GetProducts godoc
// @Summary List products (public) with pagination
// @Tags Products
// @Produce json
// @Param page query int false "Page number"
// @Param per_page query int false "Items per page"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/products [get]
func GetProducts(c fiber.Ctx) error {
pageStr := c.Query("page", "1")
perPageStr := c.Query("per_page", "10")
page, _ := strconv.Atoi(pageStr)
perPage, _ := strconv.Atoi(perPageStr)
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 10
}
if perPage > 100 {
perPage = 100
}
offset := (page - 1) * perPage
var total int64
database.DB.Model(&models.Product{}).Count(&total)
var products []models.Product
database.DB.Preload("Categories").Preload("Tags").Limit(perPage).Offset(offset).Order("created_at desc").Find(&products)
return c.JSON(fiber.Map{
"data": products,
"meta": fiber.Map{"page": page, "per_page": perPage, "total": total},
})
}
// GetProduct godoc
// @Summary Get single product (public) by slug
// @Tags Products
// @Produce json
// @Param slug path string true "Product slug"
// @Success 200 {object} models.ProductDoc
// @Failure 404 {object} map[string]string
// @Router /api/v1/products/{slug} [get]
func GetProduct(c fiber.Ctx) error {
slug := c.Params("slug")
if slug == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid slug"})
}
var product models.Product
if err := database.DB.Preload("Categories").Preload("Tags").Where("slug = ? AND deleted_at IS NULL", slug).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "product not found"})
}
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
}
return c.JSON(product)
}
// AddProductCommentRequest represents payload
type AddProductCommentRequest struct {
ProductID uint `json:"product_id" validate:"required"`
Body string `json:"body" validate:"required,min=3"`
}
func AddProductComment(c fiber.Ctx) error {
userID, ok := c.Locals("user_id").(uint)
if !ok || userID == 0 {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
var input AddProductCommentRequest
if err := c.Bind().Body(&input); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
}
// Add validation if needed
if input.Body == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "body is required"})
}
var product models.Product
if err := database.DB.First(&product, input.ProductID).Error; err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "product not found"})
}
comment := models.ProductComment{
UserID: userID,
ProductID: input.ProductID,
Body: input.Body,
}
if err := database.DB.Create(&comment).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not save comment"})
}
return c.Status(http.StatusCreated).JSON(comment)
}
// GetProductComments godoc
// @Summary Get comments for a product
// @Tags Products
// @Produce json
// @Param id path int true "Product ID"
// @Success 200 {array} models.ProductCommentDoc
// @Router /api/v1/products/{id}/comments [get]
func GetProductComments(c fiber.Ctx) error {
productID, err := strconv.Atoi(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid product id"})
}
var comments []models.ProductComment
database.DB.Where("product_id = ?", productID).Order("created_at desc").Find(&comments)
return c.JSON(comments)
}
// RecordProductCategoryView godoc
// @Summary Record a view for a product category
// @Tags Products
// @Produce json
// @Param id path int true "Category ID"
// @Success 201 {object} models.ProductCategoryViewDoc
// @Router /api/v1/product-categories/{id}/view [post]
func RecordProductCategoryView(c fiber.Ctx) error {
categoryID, err := strconv.Atoi(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid category id"})
}
var category models.ProductCategory
if err := database.DB.First(&category, categoryID).Error; err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "category not found"})
}
view := models.ProductCategoryView{
CategoryID: uint(categoryID),
IPAddress: c.IP(),
}
database.DB.Create(&view)
return c.Status(http.StatusCreated).JSON(view)
}

View File

@@ -0,0 +1,419 @@
package controllers
import (
configs "ares/config"
database "ares/database/config"
"ares/database/models"
"ares/middlewares"
"encoding/json"
"errors"
"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"`
}
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})
}
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})
}
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})
}
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})
}
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})
}
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})
}
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})
}
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})
}
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})
}
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})
}
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})
}
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})
}
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})
}
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})
}
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 {
if configs.Logger != nil {
configs.Logger.Sugar().Infof(format, args...)
}
}
}

View File

@@ -0,0 +1,150 @@
package controllers
import (
database "ares/database/config"
"ares/database/models"
"fmt"
"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)
}
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)
}
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)
}
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"})
}

798
controllers/user.go Normal file
View File

@@ -0,0 +1,798 @@
package controllers
import (
configs "ares/config"
database "ares/database/config"
"ares/database/models"
"ares/middlewares"
utils "ares/pkg/utis"
"ares/services"
"encoding/json"
"errors"
"fmt"
"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")
}
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,
})
}
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")
}
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 {
if configs.Logger != nil {
configs.Logger.Sugar().Errorf("failed to save avatar: %v", err)
}
} else {
payload["avatar_url"] = "/uploads/avatars/" + filename
}
}
} 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})
}
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,
})
}
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,
})
}
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,
},
})
}
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,
})
}
func UserOnlyExample(c fiber.Ctx) error {
claims, ok := middlewares.GetAuthClaims(c)
if !ok {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
}
return c.JSON(fiber.Map{
"message": "only normal users can access this endpoint",
"user": claims.Email,
})
}
func GoogleAuth(c fiber.Ctx) error {
if configs.AppConfig.GoogleClientID == "" {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "google oauth is not configured"})
}
stateToken, err := utils.GenerateSecureToken(16)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "state token could not be generated"})
}
authURL := "https://accounts.google.com/o/oauth2/v2/auth?" + url.Values{
"client_id": []string{configs.AppConfig.GoogleClientID},
"redirect_uri": []string{configs.AppConfig.GoogleRedirectURL},
"response_type": []string{"code"},
"scope": []string{"openid email profile"},
"state": []string{stateToken},
}.Encode()
return c.JSON(fiber.Map{"provider": "google", "auth_url": authURL, "state": stateToken})
}
func GoogleAuthCallback(c fiber.Ctx) error {
code := c.Query("code")
if code == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "google callback code is missing"})
}
// OAuth token exchange is intentionally left simple for now.
return c.JSON(fiber.Map{
"provider": "google",
"message": "google callback infrastructure is ready, token exchange can be added next",
"code": code,
"state": c.Query("state"),
})
}
func GithubAuth(c fiber.Ctx) error {
if configs.AppConfig.GithubClientID == "" {
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "github oauth is not configured"})
}
stateToken, err := utils.GenerateSecureToken(16)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "state token could not be generated"})
}
authURL := "https://github.com/login/oauth/authorize?" + url.Values{
"client_id": []string{configs.AppConfig.GithubClientID},
"redirect_uri": []string{configs.AppConfig.GithubRedirectURL},
"scope": []string{"read:user user:email"},
"state": []string{stateToken},
}.Encode()
return c.JSON(fiber.Map{"provider": "github", "auth_url": authURL, "state": stateToken})
}
func GithubAuthCallback(c fiber.Ctx) error {
code := c.Query("code")
if code == "" {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "github callback code is missing"})
}
// OAuth token exchange is intentionally left simple for now.
return c.JSON(fiber.Map{
"provider": "github",
"message": "github callback infrastructure is ready, token exchange can be added next",
"code": code,
"state": c.Query("state"),
})
}
func extractProfileName(profiles []models.Profile) (string, string) {
if len(profiles) == 0 {
return "", ""
}
return profiles[0].FirstName, profiles[0].LastName
}
func boolPtrValue(v *bool) bool {
if v == nil {
return false
}
return *v
}

View File

@@ -0,0 +1,46 @@
package database
import (
configs "ares/config"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func ConnectDB() {
dsn := configs.AppConfig.DBUrl
if dsn == "" {
if configs.Logger != nil {
configs.Logger.Warn(".env dosyasında DB_URL ayarlı değil — veritabanı bağlantısı atlanıyor (geliştirme modu)")
}
return
}
if configs.Logger != nil {
configs.Logger.Info("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.Warn), // 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 {
if configs.Logger != nil {
configs.Logger.Sugar().Errorf("MySQL veritabanı bağlantısı kurulamadı: %v", err)
}
return
}
if configs.Logger != nil {
configs.Logger.Info("MySQL veritabanı bağlantısı kuruldu.")
}
DB = db
}

View File

@@ -0,0 +1,46 @@
package database
import (
configs "ares/config"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DBPg *gorm.DB
func ConnectDBPg() {
dsn := configs.AppConfig.DBPGUrl
if dsn == "" {
if configs.Logger != nil {
configs.Logger.Warn(".env dosyasında DB_URL ayarlı değil — veritabanı bağlantısı atlanıyor (geliştirme modu)")
}
return
}
if configs.Logger != nil {
configs.Logger.Info("Yapılandırmada DB_URL_PG bulundu, veritabanına bağlanılmaya çalışılıyor...")
}
// GORM için MySQL konfigürasyonu
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn), // 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 {
if configs.Logger != nil {
configs.Logger.Sugar().Errorf("Postgres veritabanı bağlantısı kurulamadı: %v", err)
}
return
}
if configs.Logger != nil {
configs.Logger.Info("Postgres veritabanı bağlantısı kuruldu.")
}
DBPg = db
}

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

@@ -0,0 +1,117 @@
package database
import (
configs "ares/config"
"context"
"github.com/redis/go-redis/v9"
"time"
)
var RedisClient *redis.Client
var RedisOptions *redis.Options
var ctx = context.Background()
func ConnectRedis() {
redisURL := configs.AppConfig.RedisUrl
if redisURL == "" {
if configs.Logger != nil {
configs.Logger.Warn("Warning: REDIS_URL is not set, continuing without Redis cache")
}
return
}
opt, err := redis.ParseURL(redisURL)
if err != nil {
if configs.Logger != nil {
configs.Logger.Sugar().Warnf("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 {
if configs.Logger != nil {
configs.Logger.Sugar().Warnf("Warning: Failed to connect to Redis: %v, continuing without Redis cache", err)
}
RedisClient = nil
RedisOptions = nil
return
}
if configs.Logger != nil {
configs.Logger.Info("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
}
if configs.Logger != nil {
configs.Logger.Info("🧹 Clearing Redis Cache...")
}
return RedisClient.FlushDB(ctx).Err()
}

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

@@ -0,0 +1,162 @@
package migrate
import (
configs "ares/config"
database "ares/database/config"
"ares/database/models"
"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{},
&models.ProductCategory{},
&models.ProductTag{},
&models.Product{},
&models.ProductCategoryView{},
&models.ProductComment{},
&models.Cart{},
&models.CartItem{},
); err != nil {
configs.Logger.Sugar().Errorf("AutoMigrate Yapılamadı !!: %v", err)
}
seedSecurityDefaults()
configs.Logger.Info("AutoMigrate Yapıldı.")
} else {
configs.Logger.Info("DB not initialized: skipping AutoMigrate")
}
}
func MigratePg() {
if database.DBPg != nil {
if err := database.DBPg.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{},
&models.ProductCategory{},
&models.ProductTag{},
&models.Product{},
&models.ProductCategoryView{},
&models.ProductComment{},
&models.Cart{},
&models.CartItem{},
); err != nil {
configs.Logger.Sugar().Errorf("PG AutoMigrate Yapılamadı !!: %v", err)
}
seedSecurityDefaults()
configs.Logger.Info("PG AutoMigrate Yapıldı.")
} else {
configs.Logger.Info("PG 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)
seedRateLimit("global", "Global endpoint default rate limit", 1000, 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 {
configs.Logger.Sugar().Errorf("RateLimit seed failed (%s): %v", name, err)
return
}
configs.Logger.Sugar().Infof("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 {
configs.Logger.Sugar().Errorf("CorsWhitelist seed failed (%s): %v", origin, err)
return
}
configs.Logger.Sugar().Infof("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
}

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

@@ -0,0 +1,53 @@
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" form:"title"`
Images string `gorm:"type:text;not null" json:"images" form:"images"`
ImagesMid string `gorm:"type:text;not null" json:"images_mid" form:"images_mid"`
ImagesMin string `gorm:"type:text;not null" json:"images_min" form:"images_min"`
Width int `gorm:"default:0" json:"width" form:"width"`
Height int `gorm:"default:0" json:"height" form:"height"`
Quality int `gorm:"default:0" json:"quality" form:"quality"`
Format string `gorm:"type:varchar(10)" json:"format" form:"format" default:"avif"`
Content string `gorm:"type:text" json:"content,omitempty" form:"content"`
Slug string `gorm:"type:varchar(254);not null;uniqueIndex" json:"slug" form:"slug"`
Categories []Category `gorm:"many2many:post_categories;" json:"categories,omitempty" form:"categories"`
Tags []Tag `gorm:"many2many:post_tags;" json:"tags,omitempty" form:"tags"`
}
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"`
}

20
database/models/cart.go Normal file
View File

@@ -0,0 +1,20 @@
package models
import (
"gorm.io/gorm"
)
type Cart struct {
gorm.Model
UserID uint `gorm:"not null;index" json:"user_id"`
Items []CartItem `gorm:"foreignKey:CartID" json:"items,omitempty"`
}
type CartItem struct {
gorm.Model
CartID uint `gorm:"not null;index" json:"cart_id"`
Cart *Cart `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:CartID" json:"cart,omitempty"`
ProductID uint `gorm:"not null;index" json:"product_id"`
Product *Product `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:ProductID" json:"product,omitempty"`
Quantity int `gorm:"default:1" json:"quantity"`
}

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

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

View File

@@ -0,0 +1,96 @@
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"`
}
type ProductCategoryDoc struct {
ID uint `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Description string `json:"description,omitempty"`
Keywords string `json:"keywords,omitempty"`
ParentID *uint `json:"parent_id,omitempty"`
Children []ProductCategoryDoc `json:"children,omitempty"`
}
type ProductTagDoc struct {
ID uint `json:"id"`
Name string `json:"name"`
}
type ProductDoc struct {
ID uint `json:"id"`
Title string `json:"title"`
Images string `json:"images"`
Price float64 `json:"price"`
Width int `json:"width"`
Height int `json:"height"`
Quality int `json:"quality"`
Format string `json:"format"`
Content string `json:"content,omitempty"`
Slug string `json:"slug"`
Categories []ProductCategoryDoc `json:"categories,omitempty"`
Tags []ProductTagDoc `json:"tags,omitempty"`
}
type CartItemDoc struct {
ID uint `json:"id"`
CartID uint `json:"cart_id"`
ProductID uint `json:"product_id"`
Product ProductDoc `json:"product,omitempty"`
Quantity int `json:"quantity"`
}
type CartDoc struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Items []CartItemDoc `json:"items,omitempty"`
}
type ProductCommentDoc struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
ProductID uint `json:"product_id"`
Body string `json:"body"`
}
type ProductCategoryViewDoc struct {
ID uint `json:"id"`
CategoryID uint `json:"category_id"`
IPAddress string `json:"ip_address,omitempty"`
}

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

@@ -0,0 +1,23 @@
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"`
Width int `gorm:"default:0" json:"width" form:"width"`
Height int `gorm:"default:0" json:"height" form:"height"`
Quality int `gorm:"default:0" json:"quality" form:"quality"`
Format string `gorm:"type:varchar(10)" json:"format" form:"format"`
}

View File

@@ -0,0 +1,53 @@
package models
import (
"gorm.io/gorm"
)
// Minimal, temiz GORM modelleri
type ProductCategory 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"`
Keywords string `json:"keywords,omitempty"`
ParentID *uint `json:"parent_id,omitempty"`
Parent *ProductCategory `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;foreignKey:ParentID" json:"parent,omitempty"`
Children []ProductCategory `gorm:"foreignKey:ParentID" json:"children,omitempty"`
Products []Product `gorm:"many2many:product_product_categories;" json:"products,omitempty"`
}
type ProductTag struct {
gorm.Model
Name string `gorm:"type:varchar(254);not null" json:"name"`
Products []Product `gorm:"many2many:product_product_tags;" json:"products,omitempty"`
}
type Product struct {
gorm.Model
Title string `gorm:"type:varchar(254);not null" json:"title" form:"title"`
Images string `gorm:"type:text;not null" json:"images" form:"images"`
Price float64 `gorm:"type:decimal(10,2);default:0.0" json:"price" form:"price"`
Width int `gorm:"default:0" json:"width" form:"width"`
Height int `gorm:"default:0" json:"height" form:"height"`
Quality int `gorm:"default:0" json:"quality" form:"quality"`
Format string `gorm:"type:varchar(10);default:avif" json:"format" form:"format"`
Content string `gorm:"type:text" json:"content,omitempty" form:"content"`
Slug string `gorm:"type:varchar(254);not null;uniqueIndex" json:"slug" form:"slug"`
Categories []ProductCategory `gorm:"many2many:product_product_categories;" json:"categories,omitempty" form:"product_category"`
Tags []ProductTag `gorm:"many2many:product_product_tags;" json:"tags,omitempty" form:"tags"`
}
type ProductCategoryView struct {
gorm.Model
CategoryID uint `json:"category_id"`
IPAddress string `gorm:"type:varchar(45)" json:"ip_address,omitempty"`
}
type ProductComment struct {
gorm.Model
UserID uint `json:"user_id"`
ProductID uint `json:"product_id"`
Body string `gorm:"type:text" json:"body,omitempty"`
}

View File

@@ -0,0 +1,43 @@
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"`
WWidth int `gorm:"default:0" json:"w_width" form:"w_width"`
WHeight int `gorm:"default:0" json:"w_height" form:"w_height"`
WQuality int `gorm:"default:0" json:"w_quality" form:"w_quality"`
WFormat string `gorm:"type:varchar(10)" json:"w_format" form:"w_format"`
BWidth int `gorm:"default:0" json:"b_width" form:"b_width"`
BHeight int `gorm:"default:0" json:"b_height" form:"b_height"`
BQuality int `gorm:"default:0" json:"b_quality" form:"b_quality"`
BFormat string `gorm:"type:varchar(10)" json:"b_format" form:"b_format"`
}
// TableName overrides the table name used by Setting to `settings`
func (Setting) TableName() string {
return "settings"
}

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

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

75
database/seeder/seeder.go Normal file
View File

@@ -0,0 +1,75 @@
package seeder
import (
dbConfig "ares/database/config"
"ares/database/models"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// Seed checks for essential data and creates it if missing
func Seed() {
seedAdmin()
}
func seedAdmin() {
// Include soft-deleted records in lookup
var existing models.User
err := dbConfig.DB.Unscoped().Where("email = ?", "admin@example.com").First(&existing).Error
if err == nil {
// Found a user (could be soft-deleted)
// If soft-deleted, restore it
if existing.DeletedAt.Valid {
// Restore (set deleted_at to NULL) and ensure admin/verified flags
updateErr := dbConfig.DB.Unscoped().Model(&existing).Updates(map[string]interface{}{
"deleted_at": nil,
"is_admin": true,
"email_verified": true,
}).Error
if updateErr != nil {
fmt.Println("Admin restore hatası:", updateErr)
return
}
}
// user exists or restored, nothing more to do
return
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("Admin seed lookup error:", err)
return
}
// If not found at all, create
password := "password123"
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
isTrue := true
admin := models.User{
UserName: "Admin",
Email: "admin@example.com",
Password: string(hashedPassword),
IsAdmin: &isTrue,
EmailVerified: &isTrue,
}
res := dbConfig.DB.Clauses(clause.OnConflict{DoNothing: true}).Create(&admin)
if res.Error != nil {
fmt.Println("Admin seed hatası:", res.Error)
return
}
if res.RowsAffected == 0 {
// Another process likely created it concurrently
fmt.Println("Admin kullanıcı zaten mevcut; seed atlandı.")
return
}
fmt.Println("------------------------------------------------")
fmt.Println("Admin kullanıcısı oluşturuldu:")
fmt.Println("Email: admin@example.com")
fmt.Println("Şifre: password123")
fmt.Println("------------------------------------------------")
}

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

@@ -0,0 +1,20 @@
version: '3.8'
services:
app:
build: .
container_name: ares_app
# ports:
# - "8080:8080"
volumes:
- ./uploads:/app/uploads
- uploads_data:/app/uploads
env_file:
- .env
# Assumes MySQL and Redis are provided externally. Configure hosts in .env (DB_HOST, REDIS_HOST)
restart: always
volumes:
uploads_data:

26
docker-compose.yml Normal file
View File

@@ -0,0 +1,26 @@
version: '3.8'
services:
app:
build: .
container_name: ares_app
# ports:
# - "8080:8080"
volumes:
# For local development, bind mount the host uploads folder so files appear on host
#- ./uploads:/app/uploads
# For production platforms that support named volumes, you can use:
- uploads_data:/app/uploads
env_file:
- .env
# Assumes MySQL and Redis are provided externally. Configure hosts in .env (DB_HOST, REDIS_HOST)
restart: always
networks:
- dokploy-network
volumes:
uploads_data:
networks:
dokploy-network:
external: true

13
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
set -e
# Ensure uploads dir exists; try to chown but ignore failures (some platforms forbid chown)
if [ -d /app/uploads ]; then
chown -R 1000:1000 /app/uploads >/dev/null 2>&1 || true
else
mkdir -p /app/uploads >/dev/null 2>&1 || true
chown -R 1000:1000 /app/uploads >/dev/null 2>&1 || true
fi
# Execute the passed command (do not attempt to change user/groups here)
exec "$@"

1377
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

1348
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

872
docs/swagger.yaml Normal file
View File

@@ -0,0 +1,872 @@
definitions:
controllers.AddToCartRequest:
properties:
product_id:
type: integer
quantity:
minimum: 1
type: integer
required:
- product_id
- quantity
type: object
controllers.LoginRequest:
properties:
email:
type: string
password:
type: string
required:
- email
- password
type: object
controllers.RefreshRequest:
properties:
refresh_token:
type: string
required:
- refresh_token
type: object
controllers.RegisterRequest:
properties:
email:
type: string
first_name:
type: string
last_name:
type: string
password:
minLength: 6
type: string
username:
minLength: 3
type: string
required:
- email
- first_name
- last_name
- password
- username
type: object
controllers.ResendVerificationRequest:
properties:
email:
type: string
required:
- email
type: object
controllers.UpdateCartItemRequest:
properties:
quantity:
minimum: 1
type: integer
required:
- quantity
type: object
models.CartDoc:
properties:
id:
type: integer
items:
items:
$ref: '#/definitions/models.CartItemDoc'
type: array
user_id:
type: integer
type: object
models.CartItemDoc:
properties:
cart_id:
type: integer
id:
type: integer
product:
$ref: '#/definitions/models.ProductDoc'
product_id:
type: integer
quantity:
type: integer
type: object
models.CategoryDoc:
properties:
children:
items:
$ref: '#/definitions/models.CategoryDoc'
type: array
description:
type: string
id:
type: integer
parent_id:
type: integer
title:
type: string
type: object
models.CommentDoc:
properties:
body:
type: string
id:
type: integer
post_id:
type: integer
user_id:
type: integer
type: object
models.PostDoc:
properties:
categories:
items:
$ref: '#/definitions/models.CategoryDoc'
type: array
content:
type: string
id:
type: integer
images:
items:
type: string
type: array
tags:
items:
$ref: '#/definitions/models.TagDoc'
type: array
title:
type: string
type: object
models.ProductCategoryDoc:
properties:
children:
items:
$ref: '#/definitions/models.ProductCategoryDoc'
type: array
description:
type: string
id:
type: integer
keywords:
type: string
parent_id:
type: integer
slug:
type: string
title:
type: string
type: object
models.ProductCategoryViewDoc:
properties:
category_id:
type: integer
id:
type: integer
ip_address:
type: string
type: object
models.ProductCommentDoc:
properties:
body:
type: string
id:
type: integer
product_id:
type: integer
user_id:
type: integer
type: object
models.ProductDoc:
properties:
categories:
items:
$ref: '#/definitions/models.ProductCategoryDoc'
type: array
content:
type: string
format:
type: string
height:
type: integer
id:
type: integer
images:
type: string
price:
type: number
quality:
type: integer
slug:
type: string
tags:
items:
$ref: '#/definitions/models.ProductTagDoc'
type: array
title:
type: string
width:
type: integer
type: object
models.ProductTagDoc:
properties:
id:
type: integer
name:
type: string
type: object
models.TagDoc:
properties:
id:
type: integer
name:
type: string
type: object
info:
contact: {}
paths:
/api/v1/auth/login:
post:
consumes:
- application/json
parameters:
- description: Login payload
in: body
name: request
required: true
schema:
$ref: '#/definitions/controllers.LoginRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
summary: Login user
tags:
- Auth
/api/v1/auth/me:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: Get current user from token
tags:
- Auth
/api/v1/auth/refresh:
post:
consumes:
- application/json
parameters:
- description: Refresh payload
in: body
name: request
required: true
schema:
$ref: '#/definitions/controllers.RefreshRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties:
type: string
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
summary: Refresh access token
tags:
- Auth
/api/v1/auth/register:
post:
consumes:
- application/json
parameters:
- description: Register payload
in: body
name: request
required: true
schema:
$ref: '#/definitions/controllers.RegisterRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"409":
description: Conflict
schema:
additionalProperties:
type: string
type: object
summary: Register user
tags:
- Auth
/api/v1/auth/resend-verification:
post:
consumes:
- application/json
parameters:
- description: Resend verification payload
in: body
name: request
required: true
schema:
$ref: '#/definitions/controllers.ResendVerificationRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties:
type: string
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
summary: Resend verification email
tags:
- Auth
/api/v1/auth/verify-email:
get:
parameters:
- description: Email verify token
in: query
name: token
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties:
type: string
type: object
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
summary: Verify email address with token
tags:
- Auth
/api/v1/cart:
delete:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.CartDoc'
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: Clear the entire cart
tags:
- Cart
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.CartDoc'
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: Get the current user's cart
tags:
- Cart
/api/v1/cart/items:
post:
consumes:
- application/json
parameters:
- description: Cart Item Details
in: body
name: data
required: true
schema:
$ref: '#/definitions/controllers.AddToCartRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.CartDoc'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: Add item to cart
tags:
- Cart
/api/v1/cart/items/{item_id}:
delete:
parameters:
- description: Cart Item ID
in: path
name: item_id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.CartDoc'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: Remove item from cart
tags:
- Cart
put:
consumes:
- application/json
parameters:
- description: Cart Item ID
in: path
name: item_id
required: true
type: integer
- description: Update Quantity
in: body
name: data
required: true
schema:
$ref: '#/definitions/controllers.UpdateCartItemRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.CartDoc'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"401":
description: Unauthorized
schema:
additionalProperties:
type: string
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: Update cart item quantity
tags:
- Cart
/api/v1/categories:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/models.CategoryDoc'
type: array
summary: List categories (public)
tags:
- Categories
/api/v1/categories/{slug}:
get:
parameters:
- description: Category slug
in: path
name: slug
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.CategoryDoc'
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
summary: Get single category (public)
tags:
- Categories
/api/v1/comments:
get:
parameters:
- description: Post ID
in: query
name: post_id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/models.CommentDoc'
type: array
summary: List comments for a post (public)
tags:
- Comments
post:
consumes:
- application/json
parameters:
- description: Comment payload
in: body
name: data
required: true
schema:
type: object
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/models.CommentDoc'
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
summary: Create comment (public)
tags:
- Comments
/api/v1/hero:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
summary: Get active hero/banner
tags:
- Hero
/api/v1/heroes:
get:
description: Returns all hero/banner records (no filter)
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
additionalProperties: true
type: object
type: array
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
summary: Get all heroes
tags:
- Hero
/api/v1/posts:
get:
parameters:
- description: Page number
in: query
name: page
type: integer
- description: Items per page
in: query
name: per_page
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
summary: List posts (public) with pagination
tags:
- Posts
/api/v1/posts/{slug}:
get:
parameters:
- description: Post slug
in: path
name: slug
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.PostDoc'
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
summary: Get single post (public)
tags:
- Posts
/api/v1/product-categories/{id}/view:
post:
parameters:
- description: Category ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/models.ProductCategoryViewDoc'
summary: Record a view for a product category
tags:
- Products
/api/v1/products:
get:
parameters:
- description: Page number
in: query
name: page
type: integer
- description: Items per page
in: query
name: per_page
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
summary: List products (public) with pagination
tags:
- Products
/api/v1/products/{id}/comments:
get:
parameters:
- description: Product ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/models.ProductCommentDoc'
type: array
summary: Get comments for a product
tags:
- Products
/api/v1/products/{slug}:
get:
parameters:
- description: Product slug
in: path
name: slug
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.ProductDoc'
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
summary: Get single product (public) by slug
tags:
- Products
/api/v1/setting:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"404":
description: Not Found
schema:
additionalProperties:
type: string
type: object
summary: Get site settings
tags:
- Setting
/api/v1/tags:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/models.TagDoc'
type: array
summary: List tags (public)
tags:
- Tags
securityDefinitions:
BearerAuth:
description: Enter your bearer token in the format 'Bearer {token}'
in: header
name: Authorization
type: apiKey
swagger: "2.0"

70
go.mod Normal file
View File

@@ -0,0 +1,70 @@
module ares
go 1.26.0
require (
github.com/go-playground/validator/v10 v10.30.1
github.com/gofiber/fiber/v3 v3.0.0
github.com/gofiber/template/html/v2 v2.1.3
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/h2non/bimg v1.1.9
github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/v9 v9.18.0
github.com/swaggo/swag v1.16.6
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.48.0
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.2.0 // 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.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // 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/schema v1.7.0 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.2.0 // indirect
github.com/gofiber/utils/v2 v2.0.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // 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/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
)

172
go.sum Normal file
View File

@@ -0,0 +1,172 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/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-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
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/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/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
github.com/gofiber/utils v1.2.0 h1:NCaqd+Efg3khhN++eeUUTyBz+byIxAsmIjpl8kKOMIc=
github.com/gofiber/utils v1.2.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/gofiber/utils/v2 v2.0.2 h1:ShRRssz0F3AhTlAQcuEj54OEDtWF7+HJDwEi/aa6QLI=
github.com/gofiber/utils/v2 v2.0.2/go.mod h1:+9Ub4NqQ+IaJoTliq5LfdmOJAA/Hzwf4pXOxOa3RrJ0=
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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/h2non/bimg v1.1.9 h1:WH20Nxko9l/HFm4kZCA3Phbgu2cbHvYzxwxn9YROEGg=
github.com/h2non/bimg v1.1.9/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
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/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/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/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
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/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
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.7.0/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/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
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/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.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/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
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.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/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.v3 v3.0.0-20200313102051-9f266ea9e77c/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/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

122
info.log Normal file
View File

@@ -0,0 +1,122 @@
2026-02-23T21:56:56.830+0300 INFO config/loger.go:38 Logger başlatıldı (konsol + info.log)
2026-02-23T21:56:56.831+0300 INFO config/mysql_db.go:24 Yapılandırmada DB_URL bulundu, veritabanına bağlanılmaya çalışılıyor...
2026-02-23T21:56:56.840+0300 INFO config/mysql_db.go:43 MySQL veritabanı bağlantısı kuruldu.
2026-02-23T21:56:56.840+0300 INFO config/postgres_db.go:24 Yapılandırmada DB_URL_PG bulundu, veritabanına bağlanılmaya çalışılıyor...
2026-02-23T21:56:56.863+0300 INFO config/postgres_db.go:43 Postgres veritabanı bağlantısı kuruldu.
2026-02-23T21:56:56.872+0300 INFO config/redis_db.go:47 Connected to Redis successfully
2026-02-23T21:56:58.231+0300 INFO migrate/migrate.go:40 AutoMigrate Yapıldı.
2026-02-23T21:57:00.289+0300 INFO migrate/migrate.go:73 PG AutoMigrate Yapıldı.
2026-02-23T21:57:00.289+0300 INFO ares/main.go:33 Init Uygulandı !!
2026-02-23T21:57:26.962+0300 INFO middlewares/rate_limit.go:133 [rate-limit][config] loaded from db name=global active=true max=1000 window=60s
2026-02-23T21:57:26.967+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=1 max=1000 window=60s
2026-02-23T21:57:26.971+0300 INFO middlewares/rate_limit.go:133 [rate-limit][config] loaded from db name=login active=true max=10 window=60s
2026-02-23T21:57:26.976+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=login ip=127.0.0.1 count=1 max=10 window=60s
2026-02-23T21:57:38.055+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=2 max=1000 window=60s
2026-02-23T21:57:38.120+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=login ip=127.0.0.1 count=2 max=10 window=60s
2026-02-23T21:57:38.197+0300 DEBUG services/jwt_service.go:99 Generated token pair for user=6 email=beyhan@beyhan.dev access_exp=120m refresh_exp=30d
2026-02-23T21:57:38.197+0300 DEBUG services/jwt_service.go:100 access: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJzdWIiOiI2IiwiZXhwIjoxNzcxODgwMjU4LCJpYXQiOjE3NzE4NzMwNTh9.eQn5baJqBZGMc7UH2JIZe3a9iEDxlQWOKqXy0LJGt9o
2026-02-23T21:57:38.197+0300 DEBUG services/jwt_service.go:101 refresh: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJyZWZyZXNoIiwic3ViIjoiNiIsImV4cCI6MTc3NDQ2NTA1OCwiaWF0IjoxNzcxODczMDU4fQ.0TKUZEmobCOsM-Cb78ZEu2KT5hi5N7FHLUyIYvIOcKQ
2026-02-23T21:57:50.502+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=3 max=1000 window=60s
2026-02-23T21:57:50.505+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=login ip=127.0.0.1 count=3 max=10 window=60s
2026-02-23T21:57:50.580+0300 DEBUG services/jwt_service.go:99 Generated token pair for user=6 email=beyhan@beyhan.dev access_exp=120m refresh_exp=30d
2026-02-23T21:57:50.581+0300 DEBUG services/jwt_service.go:100 access: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJzdWIiOiI2IiwiZXhwIjoxNzcxODgwMjcwLCJpYXQiOjE3NzE4NzMwNzB9.7T8ohPWe5Fpz4JQzVBWyQSL1EOfB0OPIkRjdQIhKOHI
2026-02-23T21:57:50.581+0300 DEBUG services/jwt_service.go:101 refresh: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJyZWZyZXNoIiwic3ViIjoiNiIsImV4cCI6MTc3NDQ2NTA3MCwiaWF0IjoxNzcxODczMDcwfQ.0CLdSn3MN_FgF0Fd1Hsg2DJ1cOXEGxaYziav56hadJc
2026-02-23T21:58:10.580+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=4 max=1000 window=60s
2026-02-23T21:58:10.583+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=login ip=127.0.0.1 count=4 max=10 window=60s
2026-02-23T21:58:10.656+0300 DEBUG services/jwt_service.go:99 Generated token pair for user=6 email=beyhan@beyhan.dev access_exp=120m refresh_exp=30d
2026-02-23T21:58:10.656+0300 DEBUG services/jwt_service.go:100 access: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJzdWIiOiI2IiwiZXhwIjoxNzcxODgwMjkwLCJpYXQiOjE3NzE4NzMwOTB9.tDttLqMDK7Hi59vTCPNcuSglYMpVQ0Kv6QeYIG27KT0
2026-02-23T21:58:10.656+0300 DEBUG services/jwt_service.go:101 refresh: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJyZWZyZXNoIiwic3ViIjoiNiIsImV4cCI6MTc3NDQ2NTA5MCwiaWF0IjoxNzcxODczMDkwfQ.usHKEsUl_jjX4DP2Cb2guOaKh2aOyUAOHPFI_lhZtvQ
2026-02-23T21:58:31.767+0300 INFO middlewares/rate_limit.go:133 [rate-limit][config] loaded from db name=global active=true max=1000 window=60s
2026-02-23T21:58:31.772+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=1 max=1000 window=60s
2026-02-23T21:58:31.776+0300 INFO middlewares/rate_limit.go:133 [rate-limit][config] loaded from db name=login active=true max=10 window=60s
2026-02-23T21:58:31.780+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=login ip=127.0.0.1 count=1 max=10 window=60s
2026-02-23T21:58:31.857+0300 DEBUG services/jwt_service.go:99 Generated token pair for user=6 email=beyhan@beyhan.dev access_exp=120m refresh_exp=30d
2026-02-23T21:58:31.857+0300 DEBUG services/jwt_service.go:100 access: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJzdWIiOiI2IiwiZXhwIjoxNzcxODgwMzExLCJpYXQiOjE3NzE4NzMxMTF9.eaq_jE89NmrRTYmZ2zD1xo2Kf3Y6hSu-uiFxhYZvIlQ
2026-02-23T21:58:31.857+0300 DEBUG services/jwt_service.go:101 refresh: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJyZWZyZXNoIiwic3ViIjoiNiIsImV4cCI6MTc3NDQ2NTExMSwiaWF0IjoxNzcxODczMTExfQ.-WPEidNaYdXAPghF9_usm4jHP7k1YvtyHCnuOP6Vzqc
2026-02-23T22:00:27.844+0300 INFO middlewares/rate_limit.go:133 [rate-limit][config] loaded from db name=global active=true max=1000 window=60s
2026-02-23T22:00:27.848+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=1 max=1000 window=60s
2026-02-23T22:00:27.854+0300 DEBUG services/jwt_service.go:99 Generated token pair for user=6 email=beyhan@beyhan.dev access_exp=120m refresh_exp=30d
2026-02-23T22:00:27.854+0300 DEBUG services/jwt_service.go:100 access: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJzdWIiOiI2IiwiZXhwIjoxNzcxODgwNDI3LCJpYXQiOjE3NzE4NzMyMjd9.Igzy4-_1zTOgouNyOFuYB5DNoh7yVWg-BAZzEgUvpto
2026-02-23T22:00:27.854+0300 DEBUG services/jwt_service.go:101 refresh: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJyZWZyZXNoIiwic3ViIjoiNiIsImV4cCI6MTc3NDQ2NTIyNywiaWF0IjoxNzcxODczMjI3fQ.d028kzKMmbL8DMFHEsUsBObejKworjD5adaylYW1NLY
2026-02-23T22:00:27.883+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=2 max=1000 window=60s
2026-02-23T22:00:27.894+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=3 max=1000 window=60s
2026-02-23T22:00:27.932+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=4 max=1000 window=60s
2026-02-23T22:00:31.796+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-miss] key=cors:active:blacklist
2026-02-23T22:00:31.807+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-miss] key=cors:active:whitelist
2026-02-23T22:00:31.813+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/setting
2026-02-23T22:00:31.816+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=5 max=1000 window=60s
2026-02-23T22:00:31.964+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:00:31.966+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:00:31.966+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/hero
2026-02-23T22:00:31.970+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=6 max=1000 window=60s
2026-02-23T22:01:00.816+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=7 max=1000 window=60s
2026-02-23T22:01:00.829+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=8 max=1000 window=60s
2026-02-23T22:01:00.854+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=9 max=1000 window=60s
2026-02-23T22:01:02.781+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:01:02.783+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:01:02.783+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/setting
2026-02-23T22:01:02.786+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=10 max=1000 window=60s
2026-02-23T22:01:03.002+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:01:03.003+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:01:03.004+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/hero
2026-02-23T22:01:03.006+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=11 max=1000 window=60s
2026-02-23T22:04:27.027+0300 INFO middlewares/rate_limit.go:133 [rate-limit][config] loaded from db name=global active=true max=1000 window=60s
2026-02-23T22:04:27.039+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=1 max=1000 window=60s
2026-02-23T22:04:27.050+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=2 max=1000 window=60s
2026-02-23T22:04:27.064+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=3 max=1000 window=60s
2026-02-23T22:04:29.515+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-miss] key=cors:active:blacklist
2026-02-23T22:04:29.525+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-miss] key=cors:active:whitelist
2026-02-23T22:04:29.528+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/setting
2026-02-23T22:04:29.530+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=4 max=1000 window=60s
2026-02-23T22:04:29.804+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:04:29.814+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:04:29.814+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/hero
2026-02-23T22:04:29.817+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=5 max=1000 window=60s
2026-02-23T22:04:37.609+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=6 max=1000 window=60s
2026-02-23T22:04:37.626+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=7 max=1000 window=60s
2026-02-23T22:04:37.637+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=8 max=1000 window=60s
2026-02-23T22:04:39.710+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:04:39.712+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:04:39.713+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/setting
2026-02-23T22:04:39.715+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=9 max=1000 window=60s
2026-02-23T22:04:39.802+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:04:39.803+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:04:39.803+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/hero
2026-02-23T22:04:39.806+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=10 max=1000 window=60s
2026-02-23T22:04:39.894+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:04:39.899+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:04:39.899+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/products-tree/
2026-02-23T22:04:39.916+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=11 max=1000 window=60s
2026-02-23T22:04:59.678+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=12 max=1000 window=60s
2026-02-23T22:04:59.685+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=13 max=1000 window=60s
2026-02-23T22:04:59.705+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=14 max=1000 window=60s
2026-02-23T22:05:02.174+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:05:02.176+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:05:02.176+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/setting
2026-02-23T22:05:02.178+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=15 max=1000 window=60s
2026-02-23T22:05:02.382+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:05:02.384+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:05:02.384+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/hero
2026-02-23T22:05:02.386+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=16 max=1000 window=60s
2026-02-23T22:05:02.403+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:05:02.404+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:05:02.404+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/products-tree/
2026-02-23T22:05:02.407+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=17 max=1000 window=60s
2026-02-23T22:05:08.132+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=18 max=1000 window=60s
2026-02-23T22:05:08.142+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=19 max=1000 window=60s
2026-02-23T22:05:08.156+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=20 max=1000 window=60s
2026-02-23T22:05:08.281+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=21 max=1000 window=60s
2026-02-23T22:05:08.290+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=22 max=1000 window=60s
2026-02-23T22:05:08.402+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=23 max=1000 window=60s
2026-02-23T22:05:10.082+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:05:10.084+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:05:10.084+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/setting
2026-02-23T22:05:10.088+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=24 max=1000 window=60s
2026-02-23T22:05:10.199+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:05:10.200+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:05:10.200+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/hero
2026-02-23T22:05:10.205+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=25 max=1000 window=60s
2026-02-23T22:05:10.247+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:blacklist
2026-02-23T22:05:10.249+0300 INFO middlewares/dynamic_cors.go:148 [cors][cache-hit] key=cors:active:whitelist
2026-02-23T22:05:10.249+0300 INFO middlewares/dynamic_cors.go:148 [cors][allow] origin=http://localhost:3000 path=/api/v1/products-tree/
2026-02-23T22:05:10.253+0300 INFO middlewares/rate_limit.go:133 [rate-limit][allow] name=global ip=127.0.0.1 count=26 max=1000 window=60s

130
main.go Normal file
View File

@@ -0,0 +1,130 @@
package main
// Swagger security definition for Bearer token (swaggo)
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Enter your bearer token in the format 'Bearer {token}'
import (
config "ares/config"
database "ares/database/config"
"ares/database/migrate"
_ "ares/docs"
"ares/routes"
"os"
"time"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/static"
"github.com/gofiber/template/html/v2"
swag "github.com/swaggo/swag"
)
func init() {
config.LoadConfig()
database.ConnectDB()
database.ConnectDBPg()
database.ConnectRedis()
migrate.Migrate()
migrate.MigratePg()
//seeder.Seed()
if config.Logger != nil {
config.Logger.Info("Init Uygulandı !!")
}
}
func main() {
// Initialize standard Go html template engine
// Use absolute path for safety
cwd, _ := os.Getwd()
engine := html.New(cwd+"/views", ".html")
// Add helper function to handle *bool and bool in templates
engine.AddFunc("isTrue", func(val interface{}) bool {
if val == nil {
return false
}
switch v := val.(type) {
case bool:
return v
case *bool:
if v == nil {
return false
}
return *v
default:
return false
}
})
engine.Reload(true) // Enable reload for development
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,
Views: engine, // Register the views engine
})
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)
})
// Serve static files from public directory (CSS, JS, Assets)
app.Use("/", static.New("./public"))
// Register Routes
// Serve generated swagger.json from registered docs (swag)
app.Get("/swagger/swagger.json", func(c fiber.Ctx) error {
doc, err := swag.ReadDoc("swagger")
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "swagger doc not found"})
}
return c.Type("json").SendString(doc)
})
// Simple Swagger UI page using CDN (points to /swagger/swagger.json)
app.Get("/swagger/*", func(c fiber.Ctx) error {
html := `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Swagger UI</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4/swagger-ui-bundle.js"></script>
<script>
window.ui = SwaggerUIBundle({
url: '/swagger/swagger.json',
dom_id: '#swagger-ui'
})
</script>
</body>
</html>`
return c.Type("html").SendString(html)
})
routes.RouterUser(app)
routes.RouterAdmin(app)
// Start the server using port from config
if config.AppConfig != nil && config.AppConfig.Port != "" {
if err := app.Listen(":" + config.AppConfig.Port); err != nil {
config.Logger.Sugar().Fatalf("Server başlatılamadı: %v", err)
}
} else {
if err := app.Listen(":8080"); err != nil {
config.Logger.Sugar().Fatalf("Server başlatılamadı: %v", err)
}
}
}

View File

@@ -0,0 +1,108 @@
package middlewares
import (
"strings"
"ares/services"
"github.com/gofiber/fiber/v3"
)
const authClaimsKey = "auth_claims"
func RequireAuth(c fiber.Ctx) error {
// First try Authorization header (Bearer JWT)
authHeader := strings.TrimSpace(c.Get("Authorization"))
if authHeader != "" {
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || strings.TrimSpace(parts[1]) == "" {
// If request originates from browser/HTMX, redirect to login instead of returning JSON
if c.Get("HX-Request") == "true" {
c.Set("HX-Redirect", "/login")
return c.SendStatus(fiber.StatusOK)
}
accept := strings.ToLower(c.Get("Accept"))
if strings.Contains(accept, "text/html") || strings.HasPrefix(c.Path(), "/admin") {
return c.Redirect().To("/login")
}
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 {
if c.Get("HX-Request") == "true" {
c.Set("HX-Redirect", "/login")
return c.SendStatus(fiber.StatusOK)
}
accept := strings.ToLower(c.Get("Accept"))
if strings.Contains(accept, "text/html") || strings.HasPrefix(c.Path(), "/admin") {
return c.Redirect().To("/login")
}
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid token"})
}
if claims.TokenType != services.TokenTypeAccess {
if c.Get("HX-Request") == "true" {
c.Set("HX-Redirect", "/login")
return c.SendStatus(fiber.StatusOK)
}
accept := strings.ToLower(c.Get("Accept"))
if strings.Contains(accept, "text/html") || strings.HasPrefix(c.Path(), "/admin") {
return c.Redirect().To("/login")
}
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "access token required"})
}
c.Locals(authClaimsKey, claims)
return c.Next()
}
// Fallback: check cookie-based admin session (browser login) — expect signed JWT
cookie := c.Cookies("admin_session")
if cookie != "" {
jwtService := services.NewJWTService()
if claims, err := jwtService.ValidateToken(cookie); err == nil {
if claims.TokenType == services.TokenTypeAccess {
c.Locals(authClaimsKey, claims)
return c.Next()
}
}
}
// Default unauthorized response: redirect to login for browser requests, JSON for API clients
if c.Get("HX-Request") == "true" {
c.Set("HX-Redirect", "/login")
return c.SendStatus(fiber.StatusOK)
}
accept := strings.ToLower(c.Get("Accept"))
if strings.Contains(accept, "text/html") || strings.HasPrefix(c.Path(), "/admin") {
return c.Redirect().To("/login")
}
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "authorization header is required"})
}
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
}

151
middlewares/dynamic_cors.go Normal file
View File

@@ -0,0 +1,151 @@
package middlewares
import (
configs "ares/config"
database "ares/database/config"
"ares/database/models"
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/gofiber/fiber/v3"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
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] {
// use project logger if available
if configs.Logger != nil {
configs.Logger.Warn("cors blocked - blacklist", zapFieldsForCORS(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] {
if configs.Logger != nil {
configs.Logger.Warn("cors blocked - not whitelisted", zapFieldsForCORS(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 zapFieldsForCORS(origin, path string) []zap.Field {
return []zap.Field{
zap.String("origin", origin),
zap.String("path", path),
}
}
func corsLogf(format string, args ...interface{}) {
if configs.AppConfig != nil && configs.AppConfig.CorsDebug {
if configs.Logger != nil {
configs.Logger.Sugar().Infof(format, args...)
}
}
}

136
middlewares/rate_limit.go Normal file
View File

@@ -0,0 +1,136 @@
package middlewares
import (
configs "ares/config"
database "ares/database/config"
"ares/database/models"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v3"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"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))
if configs.Logger != nil {
configs.Logger.Warn("rate-limit blocked", zapFieldsForRateLimit(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 zapFieldsForRateLimit(name, ip string, count, max int64, window int) []zap.Field {
return []zap.Field{
zap.String("name", name),
zap.String("ip", ip),
zap.Int64("count", count),
zap.Int64("max", max),
zap.Int("window_seconds", window),
}
}
func rateLimitLogf(format string, args ...interface{}) {
if configs.AppConfig != nil && configs.AppConfig.CorsDebug {
if configs.Logger != nil {
configs.Logger.Sugar().Infof(format, args...)
}
}
}

View File

@@ -0,0 +1,12 @@
package middlewares
import (
"github.com/gofiber/fiber/v3"
)
// RejectAll middleware tüm gelen istekleri reddeder ve HTTP 500 döner.
func RejectAll() fiber.Handler {
return func(c fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "internal server error"})
}
}

48
pkg/utis/slug.go Normal file
View File

@@ -0,0 +1,48 @@
package utils
import (
"regexp"
"strings"
"unicode"
)
// Slugify converts a string to a URL-friendly slug, replacing Turkish characters
// with ASCII equivalents and ensuring lowercase, hyphen-separated result.
func Slugify(s string) string {
if s == "" {
return ""
}
// Map Turkish characters to ASCII
replacer := strings.NewReplacer(
"ç", "c", "Ç", "c",
"ğ", "g", "Ğ", "g",
"ı", "i", "İ", "i",
"ö", "o", "Ö", "o",
"ş", "s", "Ş", "s",
"ü", "u", "Ü", "u",
"", "", "'", "",
)
s = replacer.Replace(s)
// Normalize: keep letters, numbers, and spaces
var b strings.Builder
for _, r := range strings.TrimSpace(s) {
if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsSpace(r) {
b.WriteRune(r)
} else {
// convert other punctuation to space
b.WriteRune(' ')
}
}
out := strings.ToLower(b.String())
// replace spaces with hyphens and collapse multiple hyphens
out = strings.TrimSpace(out)
// replace any sequence of non-alnum with hyphen
re := regexp.MustCompile(`[^a-z0-9]+`)
out = re.ReplaceAllString(out, "-")
out = strings.Trim(out, "-")
return out
}

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

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

146
public/admin/css/theme.css Normal file
View File

@@ -0,0 +1,146 @@
/* Admin Panel Theme CSS */
:root {
--sidebar-width: 250px;
--header-height: 60px;
--transition-speed: 0.3s;
}
[data-bs-theme="light"] {
--bg-body: #f8f9fa;
--bg-sidebar: #ffffff;
--text-sidebar: #333333;
--border-color: #e9ecef;
--accent-color: #0d6efd;
--accent-color-rgb: 13,110,253;
}
[data-bs-theme="dark"] {
--bg-body: #212529;
--bg-sidebar: #1a1d20;
--text-sidebar: #e9ecef;
--border-color: #343a40;
--accent-color: #ffc107; /* Şeftali tonu veya sarımsı */
--accent-color-rgb: 255,193,7;
}
body {
background-color: var(--bg-body);
transition: background-color var(--transition-speed);
}
/* Sidebar Styles */
#sidebar {
width: var(--sidebar-width);
height: 100vh;
position: fixed;
top: 0;
left: 0;
background-color: var(--bg-sidebar);
color: var(--text-sidebar);
border-right: 1px solid var(--border-color);
transition: transform var(--transition-speed);
z-index: 1040;
overflow-y: auto;
}
.sidebar-header {
height: var(--header-height);
display: flex;
align-items: center;
padding: 0 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.sidebar-nav .nav-link {
color: var(--text-sidebar);
padding: 0.75rem 1.5rem;
border-left: 3px solid transparent;
}
.sidebar-nav .nav-link:hover,
.sidebar-nav .nav-link.active {
background-color: rgba(var(--accent-color-rgb), 0.1);
border-left-color: var(--accent-color);
color: var(--accent-color);
}
/* Header Styles */
#main-header {
height: var(--header-height);
position: fixed;
top: 0;
left: var(--sidebar-width);
right: 0;
background-color: var(--bg-sidebar);
border-bottom: 1px solid var(--border-color);
z-index: 1030;
transition: left var(--transition-speed);
display: flex;
align-items: center;
padding: 0 1.5rem;
}
/* Main Content Area */
#main-content-wrapper {
margin-left: var(--sidebar-width);
padding-top: var(--header-height);
transition: margin-left var(--transition-speed);
min-height: 100vh;
}
/* Mobile Sidebar Behavior */
@media (max-width: 991.98px) {
#sidebar {
transform: translateX(-100%);
}
#sidebar.show {
transform: translateX(0);
}
#main-header {
left: 0;
}
#main-content-wrapper {
margin-left: 0;
}
}
/* Utilities */
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Turnstile Widget Placeholder Style */
.cf-turnstile {
margin: 1rem 0;
min-height: 65px;
}
/* Logo preview helpers: force preview backgrounds for white/black logos */
.logo-preview {
display: inline-block;
padding: 0.5rem;
border-radius: 0.25rem;
}
.logo-preview-dark {
/* stronger, always-visible dark preview (for white logos) */
background-color: rgba(0, 0, 0, 0.6) !important;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
.logo-preview-light {
/* white preview for dark/black logos */
background-color: #ffffff !important;
border: 1px solid rgba(0,0,0,0.08);
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
}

63
public/admin/js/main.js Normal file
View File

@@ -0,0 +1,63 @@
document.addEventListener('alpine:init', () => {
Alpine.store('theme', {
mode: localStorage.getItem('theme') || 'light',
toggle() {
this.mode = this.mode === 'light' ? 'dark' : 'light';
localStorage.setItem('theme', this.mode);
this.apply();
},
apply() {
document.documentElement.setAttribute('data-bs-theme', this.mode);
}
});
// Apply theme on init
Alpine.store('theme').apply();
Alpine.store('sidebar', {
open: false,
toggle() {
this.open = !this.open;
const sidebar = document.getElementById('sidebar');
if (this.open) {
sidebar.classList.add('show');
} else {
sidebar.classList.remove('show');
}
},
close() {
this.open = false;
document.getElementById('sidebar').classList.remove('show');
}
});
});
// HTMX Configuration
document.addEventListener('htmx:configRequest', (event) => {
// Add CSRF token if available (implementation dependent)
// event.detail.headers['X-CSRF-Token'] = getCsrfToken();
});
document.addEventListener('htmx:beforeSwap', (event) => {
// Handle 401 Unauthorized by redirecting to login
if (event.detail.xhr.status === 401) {
window.location.href = '/login';
}
});
// Mobile helper: Close sidebar on outside click (simple implementation)
document.addEventListener('click', (e) => {
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.querySelector('[data-bs-target="#sidebar"]'); // Adjust selector as needed
// Only on mobile
if (window.innerWidth < 992 && Alpine.store('sidebar').open) {
if (!sidebar.contains(e.target) && !toggleBtn.contains(e.target)) {
Alpine.store('sidebar').close();
}
}
});

77
public/assets/.package-lock.json generated Normal file
View File

@@ -0,0 +1,77 @@
{
"name": "scripts",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@vue/reactivity": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.1.5"
}
},
"node_modules/@vue/shared": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
"license": "MIT"
},
"node_modules/alpinejs": {
"version": "3.15.8",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.8.tgz",
"integrity": "sha512-zxIfCRTBGvF1CCLIOMQOxAyBuqibxSEwS6Jm1a3HGA9rgrJVcjEWlwLcQTVGAWGS8YhAsTRLVrtQ5a5QT9bSSQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
},
"node_modules/bootstrap": {
"version": "5.3.8",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/htmx": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/htmx/-/htmx-0.0.2.tgz",
"integrity": "sha512-FfUo3ynRYr6Ra4vqmS4Nq9g47607FSmvHYCOuU8bvbW8s4kPMhAmCbMBjuW2cEZI6DauaFNZKinfgV91cc9Feg==",
"license": "MIT",
"bin": {
"htmx": "htmx.js"
}
},
"node_modules/jquery": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-4.0.0.tgz",
"integrity": "sha512-TXCHVR3Lb6TZdtw1l3RTLf8RBWVGexdxL6AC8/e0xZKEpBflBsjh9/8LXw+dkNFuOyW9B7iB3O1sP7hS0Kiacg==",
"license": "MIT"
}
}
}

View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2019 Federico Zivolo
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,376 @@
<!-- <HEADER> // IGNORE IT -->
<p align="center">
<img src="https://rawcdn.githack.com/popperjs/popper-core/8805a5d7599e14619c9e7ac19a3713285d8e5d7f/docs/src/images/popper-logo-outlined.svg" alt="Popper" height="300px"/>
</p>
<div align="center">
<h1>Tooltip & Popover Positioning Engine</h1>
</div>
<p align="center">
<a href="https://www.npmjs.com/package/@popperjs/core">
<img src="https://img.shields.io/npm/v/@popperjs/core?style=for-the-badge" alt="npm version" />
</a>
<a href="https://www.npmjs.com/package/@popperjs/core">
<img src="https://img.shields.io/endpoint?style=for-the-badge&url=https://runkit.io/fezvrasta/combined-npm-downloads/1.0.0?packages=popper.js,@popperjs/core" alt="npm downloads per month (popper.js + @popperjs/core)" />
</a>
<a href="https://rollingversions.com/popperjs/popper-core">
<img src="https://img.shields.io/badge/Rolling%20Versions-Enabled-brightgreen?style=for-the-badge" alt="Rolling Versions" />
</a>
</p>
<br />
<!-- </HEADER> // NOW BEGINS THE README -->
**Positioning tooltips and popovers is difficult. Popper is here to help!**
Given an element, such as a button, and a tooltip element describing it, Popper
will automatically put the tooltip in the right place near the button.
It will position _any_ UI element that "pops out" from the flow of your document
and floats near a target element. The most common example is a tooltip, but it
also includes popovers, drop-downs, and more. All of these can be generically
described as a "popper" element.
## Demo
[![Popper visualized](https://i.imgur.com/F7qWsmV.jpg)](https://popper.js.org)
## Docs
- [v2.x (latest)](https://popper.js.org/docs/v2/)
- [v1.x](https://popper.js.org/docs/v1/)
We've created a
[Migration Guide](https://popper.js.org/docs/v2/migration-guide/) to help you
migrate from Popper 1 to Popper 2.
To contribute to the Popper website and documentation, please visit the
[dedicated repository](https://github.com/popperjs/website).
## Why not use pure CSS?
- **Clipping and overflow issues**: Pure CSS poppers will not be prevented from
overflowing clipping boundaries, such as the viewport. It will get partially
cut off or overflows if it's near the edge since there is no dynamic
positioning logic. When using Popper, your popper will always be positioned in
the right place without needing manual adjustments.
- **No flipping**: CSS poppers will not flip to a different placement to fit
better in view if necessary. While you can manually adjust for the main axis
overflow, this feature cannot be achieved via CSS alone. Popper automatically
flips the tooltip to make it fit in view as best as possible for the user.
- **No virtual positioning**: CSS poppers cannot follow the mouse cursor or be
used as a context menu. Popper allows you to position your tooltip relative to
any coordinates you desire.
- **Slower development cycle**: When pure CSS is used to position popper
elements, the lack of dynamic positioning means they must be carefully placed
to consider overflow on all screen sizes. In reusable component libraries,
this means a developer can't just add the component anywhere on the page,
because these issues need to be considered and adjusted for every time. With
Popper, you can place your elements anywhere and they will be positioned
correctly, without needing to consider different screen sizes, layouts, etc.
This massively speeds up development time because this work is automatically
offloaded to Popper.
- **Lack of extensibility**: CSS poppers cannot be easily extended to fit any
arbitrary use case you may need to adjust for. Popper is built with
extensibility in mind.
## Why Popper?
With the CSS drawbacks out of the way, we now move on to Popper in the
JavaScript space itself.
Naive JavaScript tooltip implementations usually have the following problems:
- **Scrolling containers**: They don't ensure the tooltip stays with the
reference element while scrolling when inside any number of scrolling
containers.
- **DOM context**: They often require the tooltip move outside of its original
DOM context because they don't handle `offsetParent` contexts.
- **Compatibility**: Popper handles an incredible number of edge cases regarding
different browsers and environments (mobile viewports, RTL, scrollbars enabled
or disabled, etc.). Popper is a popular and well-maintained library, so you
can be confident positioning will work for your users on any device.
- **Configurability**: They often lack advanced configurability to suit any
possible use case.
- **Size**: They are usually relatively large in size, or require an ancient
jQuery dependency.
- **Performance**: They often have runtime performance issues and update the
tooltip position too slowly.
**Popper solves all of these key problems in an elegant, performant manner.** It
is a lightweight ~3 kB library that aims to provide a reliable and extensible
positioning engine you can use to ensure all your popper elements are positioned
in the right place.
When you start writing your own popper implementation, you'll quickly run into
all of the problems mentioned above. These widgets are incredibly common in our
UIs; we've done the hard work figuring this out so you don't need to spend hours
fixing and handling numerous edge cases that we already ran into while building
the library!
Popper is used in popular libraries like Bootstrap, Foundation, Material UI, and
more. It's likely you've already used popper elements on the web positioned by
Popper at some point in the past few years.
Since we write UIs using powerful abstraction libraries such as React or Angular
nowadays, you'll also be glad to know Popper can fully integrate with them and
be a good citizen together with your other components. Check out `react-popper`
for the official Popper wrapper for React.
## Installation
### 1. Package Manager
```bash
# With npm
npm i @popperjs/core
# With Yarn
yarn add @popperjs/core
```
### 2. CDN
```html
<!-- Development version -->
<script src="https://unpkg.com/@popperjs/core@2/dist/umd/popper.js"></script>
<!-- Production version -->
<script src="https://unpkg.com/@popperjs/core@2"></script>
```
### 3. Direct Download?
Managing dependencies by "directly downloading" them and placing them into your
source code is not recommended for a variety of reasons, including missing out
on feat/fix updates easily. Please use a versioning management system like a CDN
or npm/Yarn.
## Usage
The most straightforward way to get started is to import Popper from the `unpkg`
CDN, which includes all of its features. You can call the `Popper.createPopper`
constructor to create new popper instances.
Here is a complete example:
```html
<!DOCTYPE html>
<title>Popper example</title>
<style>
#tooltip {
background-color: #333;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 13px;
}
</style>
<button id="button" aria-describedby="tooltip">I'm a button</button>
<div id="tooltip" role="tooltip">I'm a tooltip</div>
<script src="https://unpkg.com/@popperjs/core@^2.0.0"></script>
<script>
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
// Pass the button, the tooltip, and some options, and Popper will do the
// magic positioning for you:
Popper.createPopper(button, tooltip, {
placement: 'right',
});
</script>
```
Visit the [tutorial](https://popper.js.org/docs/v2/tutorial/) for an example of
how to build your own tooltip from scratch using Popper.
### Module bundlers
You can import the `createPopper` constructor from the fully-featured file:
```js
import { createPopper } from '@popperjs/core';
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
// Pass the button, the tooltip, and some options, and Popper will do the
// magic positioning for you:
createPopper(button, tooltip, {
placement: 'right',
});
```
All the modifiers listed in the docs menu will be enabled and "just work", so
you don't need to think about setting Popper up. The size of Popper including
all of its features is about 5 kB minzipped, but it may grow a bit in the
future.
#### Popper Lite (tree-shaking)
If bundle size is important, you'll want to take advantage of tree-shaking. The
library is built in a modular way to allow to import only the parts you really
need.
```js
import { createPopperLite as createPopper } from '@popperjs/core';
```
The Lite version includes the most necessary modifiers that will compute the
offsets of the popper, compute and add the positioning styles, and add event
listeners. This is close in bundle size to pure CSS tooltip libraries, and
behaves somewhat similarly.
However, this does not include the features that makes Popper truly useful.
The two most useful modifiers not included in Lite are `preventOverflow` and
`flip`:
```js
import {
createPopperLite as createPopper,
preventOverflow,
flip,
} from '@popperjs/core';
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
createPopper(button, tooltip, {
modifiers: [preventOverflow, flip],
});
```
As you make more poppers, you may be finding yourself needing other modifiers
provided by the library.
See [tree-shaking](https://popper.js.org/docs/v2/performance/#tree-shaking) for more
information.
## Distribution targets
Popper is distributed in 3 different versions, in 3 different file formats.
The 3 file formats are:
- `esm` (works with `import` syntax — **recommended**)
- `umd` (works with `<script>` tags or RequireJS)
- `cjs` (works with `require()` syntax)
There are two different `esm` builds, one for bundler consumers (e.g. webpack,
Rollup, etc..), which is located under `/lib`, and one for browsers with native
support for ES Modules, under `/dist/esm`. The only difference within the two,
is that the browser-compatible version doesn't make use of
`process.env.NODE_ENV` to run development checks.
The 3 versions are:
- `popper`: includes all the modifiers (features) in one file (**default**);
- `popper-lite`: includes only the minimum amount of modifiers to provide the
basic functionality;
- `popper-base`: doesn't include any modifier, you must import them separately;
Below you can find the size of each version, minified and compressed with the
[Brotli compression algorithm](https://medium.com/groww-engineering/enable-brotli-compression-in-webpack-with-fallback-to-gzip-397a57cf9fc6):
<!-- Don't change the labels to use hyphens, it breaks, even when encoded -->
![](https://badge-size.now.sh/https://unpkg.com/@popperjs/core/dist/umd/popper.min.js?compression=brotli&label=popper)
![](https://badge-size.now.sh/https://unpkg.com/@popperjs/core/dist/umd/popper-lite.min.js?compression=brotli&label=popper%20lite)
![](https://badge-size.now.sh/https://unpkg.com/@popperjs/core/dist/umd/popper-base.min.js?compression=brotli&label=popper%20base)
## Hacking the library
If you want to play with the library, implement new features, fix a bug you
found, or simply experiment with it, this section is for you!
First of all, make sure to have
[Yarn installed](https://yarnpkg.com/lang/en/docs/install).
Install the development dependencies:
```bash
yarn install
```
And run the development environment:
```bash
yarn dev
```
Then, simply open one the development server web page:
```bash
# macOS and Linux
open localhost:5000
# Windows
start localhost:5000
```
From there, you can open any of the examples (`.html` files) to fiddle with
them.
Now any change you will made to the source code, will be automatically compiled,
you just need to refresh the page.
If the page is not working properly, try to go in _"Developer Tools >
Application > Clear storage"_ and click on "_Clear site data_".
To run the examples you need a browser with
[JavaScript modules via script tag support](https://caniuse.com/#feat=es6-module).
## Test Suite
Popper is currently tested with unit tests, and functional tests. Both of them
are run by Jest.
### Unit Tests
The unit tests use JSDOM to provide a primitive document object API, they are
used to ensure the utility functions behave as expected in isolation.
### Functional Tests
The functional tests run with Puppeteer, to take advantage of a complete browser
environment. They are currently running on Chromium, and Firefox.
You can run them with `yarn test:functional`. Set the `PUPPETEER_BROWSER`
environment variable to `firefox` to run them on the Mozilla browser.
The assertions are written in form of image snapshots, so that it's easy to
assert for the correct Popper behavior without having to write a lot of offsets
comparisons manually.
You can mark a `*.test.js` file to run in the Puppeteer environment by
prepending a `@jest-environment puppeteer` JSDoc comment to the interested file.
Here's an example of a basic functional test:
```js
/**
* @jest-environment puppeteer
* @flow
*/
import { screenshot } from '../utils/puppeteer.js';
it('should position the popper on the right', async () => {
const page = await browser.newPage();
await page.goto(`${TEST_URL}/basic.html`);
expect(await screenshot(page)).toMatchImageSnapshot();
});
```
You can find the complete
[`jest-puppeteer` documentation here](https://github.com/smooth-code/jest-puppeteer#api),
and the
[`jest-image-snapshot` documentation here](https://github.com/americanexpress/jest-image-snapshot#%EF%B8%8F-api).
## License
MIT

View File

@@ -0,0 +1,65 @@
/**
* @popperjs/core v2.11.8 - MIT License
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var top = 'top';
var bottom = 'bottom';
var right = 'right';
var left = 'left';
var auto = 'auto';
var basePlacements = [top, bottom, right, left];
var start = 'start';
var end = 'end';
var clippingParents = 'clippingParents';
var viewport = 'viewport';
var popper = 'popper';
var reference = 'reference';
var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {
return acc.concat([placement + "-" + start, placement + "-" + end]);
}, []);
var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {
return acc.concat([placement, placement + "-" + start, placement + "-" + end]);
}, []); // modifiers that need to read the DOM
var beforeRead = 'beforeRead';
var read = 'read';
var afterRead = 'afterRead'; // pure-logic modifiers
var beforeMain = 'beforeMain';
var main = 'main';
var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)
var beforeWrite = 'beforeWrite';
var write = 'write';
var afterWrite = 'afterWrite';
var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];
exports.afterMain = afterMain;
exports.afterRead = afterRead;
exports.afterWrite = afterWrite;
exports.auto = auto;
exports.basePlacements = basePlacements;
exports.beforeMain = beforeMain;
exports.beforeRead = beforeRead;
exports.beforeWrite = beforeWrite;
exports.bottom = bottom;
exports.clippingParents = clippingParents;
exports.end = end;
exports.left = left;
exports.main = main;
exports.modifierPhases = modifierPhases;
exports.placements = placements;
exports.popper = popper;
exports.read = read;
exports.reference = reference;
exports.right = right;
exports.start = start;
exports.top = top;
exports.variationPlacements = variationPlacements;
exports.viewport = viewport;
exports.write = write;
//# sourceMappingURL=enums.js.map

View File

@@ -0,0 +1,3 @@
// @flow
export * from '../../lib/enums.js'

View File

@@ -0,0 +1 @@
{"version":3,"file":"enums.js","sources":["../../src/enums.js"],"sourcesContent":["// @flow\nexport const top: 'top' = 'top';\nexport const bottom: 'bottom' = 'bottom';\nexport const right: 'right' = 'right';\nexport const left: 'left' = 'left';\nexport const auto: 'auto' = 'auto';\nexport type BasePlacement =\n | typeof top\n | typeof bottom\n | typeof right\n | typeof left;\nexport const basePlacements: Array<BasePlacement> = [top, bottom, right, left];\n\nexport const start: 'start' = 'start';\nexport const end: 'end' = 'end';\nexport type Variation = typeof start | typeof end;\n\nexport const clippingParents: 'clippingParents' = 'clippingParents';\nexport const viewport: 'viewport' = 'viewport';\nexport type Boundary = Element | Array<Element> | typeof clippingParents;\nexport type RootBoundary = typeof viewport | 'document';\n\nexport const popper: 'popper' = 'popper';\nexport const reference: 'reference' = 'reference';\nexport type Context = typeof popper | typeof reference;\n\nexport type VariationPlacement =\n | 'top-start'\n | 'top-end'\n | 'bottom-start'\n | 'bottom-end'\n | 'right-start'\n | 'right-end'\n | 'left-start'\n | 'left-end';\nexport type AutoPlacement = 'auto' | 'auto-start' | 'auto-end';\nexport type ComputedPlacement = VariationPlacement | BasePlacement;\nexport type Placement = AutoPlacement | BasePlacement | VariationPlacement;\n\nexport const variationPlacements: Array<VariationPlacement> = basePlacements.reduce(\n (acc: Array<VariationPlacement>, placement: BasePlacement) =>\n acc.concat([(`${placement}-${start}`: any), (`${placement}-${end}`: any)]),\n []\n);\nexport const placements: Array<Placement> = [...basePlacements, auto].reduce(\n (\n acc: Array<Placement>,\n placement: BasePlacement | typeof auto\n ): Array<Placement> =>\n acc.concat([\n placement,\n (`${placement}-${start}`: any),\n (`${placement}-${end}`: any),\n ]),\n []\n);\n\n// modifiers that need to read the DOM\nexport const beforeRead: 'beforeRead' = 'beforeRead';\nexport const read: 'read' = 'read';\nexport const afterRead: 'afterRead' = 'afterRead';\n// pure-logic modifiers\nexport const beforeMain: 'beforeMain' = 'beforeMain';\nexport const main: 'main' = 'main';\nexport const afterMain: 'afterMain' = 'afterMain';\n// modifier with the purpose to write to the DOM (or write into a framework state)\nexport const beforeWrite: 'beforeWrite' = 'beforeWrite';\nexport const write: 'write' = 'write';\nexport const afterWrite: 'afterWrite' = 'afterWrite';\nexport const modifierPhases: Array<ModifierPhases> = [\n beforeRead,\n read,\n afterRead,\n beforeMain,\n main,\n afterMain,\n beforeWrite,\n write,\n afterWrite,\n];\n\nexport type ModifierPhases =\n | typeof beforeRead\n | typeof read\n | typeof afterRead\n | typeof beforeMain\n | typeof main\n | typeof afterMain\n | typeof beforeWrite\n | typeof write\n | typeof afterWrite;\n"],"names":["top","bottom","right","left","auto","basePlacements","start","end","clippingParents","viewport","popper","reference","variationPlacements","reduce","acc","placement","concat","placements","beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite","modifierPhases"],"mappings":";;;;;;;;IACaA,GAAU,GAAG;IACbC,MAAgB,GAAG;IACnBC,KAAc,GAAG;IACjBC,IAAY,GAAG;IACfC,IAAY,GAAG;IAMfC,cAAoC,GAAG,CAACL,GAAD,EAAMC,MAAN,EAAcC,KAAd,EAAqBC,IAArB;IAEvCG,KAAc,GAAG;IACjBC,GAAU,GAAG;IAGbC,eAAkC,GAAG;IACrCC,QAAoB,GAAG;IAIvBC,MAAgB,GAAG;IACnBC,SAAsB,GAAG;IAgBzBC,mBAA8C,gBAAGP,cAAc,CAACQ,MAAf,CAC5D,UAACC,GAAD,EAAiCC,SAAjC;AAAA,SACED,GAAG,CAACE,MAAJ,CAAW,CAAKD,SAAL,SAAkBT,KAAlB,EAAqCS,SAArC,SAAkDR,GAAlD,CAAX,CADF;AAAA,CAD4D,EAG5D,EAH4D;IAKjDU,UAA4B,gBAAG,UAAIZ,cAAJ,GAAoBD,IAApB,GAA0BS,MAA1B,CAC1C,UACEC,GADF,EAEEC,SAFF;AAAA,SAIED,GAAG,CAACE,MAAJ,CAAW,CACTD,SADS,EAELA,SAFK,SAEQT,KAFR,EAGLS,SAHK,SAGQR,GAHR,CAAX,CAJF;AAAA,CAD0C,EAU1C,EAV0C;;IAc/BW,UAAwB,GAAG;IAC3BC,IAAY,GAAG;IACfC,SAAsB,GAAG;;IAEzBC,UAAwB,GAAG;IAC3BC,IAAY,GAAG;IACfC,SAAsB,GAAG;;IAEzBC,WAA0B,GAAG;IAC7BC,KAAc,GAAG;IACjBC,UAAwB,GAAG;IAC3BC,cAAqC,GAAG,CACnDT,UADmD,EAEnDC,IAFmD,EAGnDC,SAHmD,EAInDC,UAJmD,EAKnDC,IALmD,EAMnDC,SANmD,EAOnDC,WAPmD,EAQnDC,KARmD,EASnDC,UATmD;;;;;;;;;;;;;;;;;;;;;;;;;"}

View File

@@ -0,0 +1,939 @@
/**
* @popperjs/core v2.11.8 - MIT License
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function getWindow(node) {
if (node == null) {
return window;
}
if (node.toString() !== '[object Window]') {
var ownerDocument = node.ownerDocument;
return ownerDocument ? ownerDocument.defaultView || window : window;
}
return node;
}
function isElement(node) {
var OwnElement = getWindow(node).Element;
return node instanceof OwnElement || node instanceof Element;
}
function isHTMLElement(node) {
var OwnElement = getWindow(node).HTMLElement;
return node instanceof OwnElement || node instanceof HTMLElement;
}
function isShadowRoot(node) {
// IE 11 has no ShadowRoot
if (typeof ShadowRoot === 'undefined') {
return false;
}
var OwnElement = getWindow(node).ShadowRoot;
return node instanceof OwnElement || node instanceof ShadowRoot;
}
var max = Math.max;
var min = Math.min;
var round = Math.round;
function getUAString() {
var uaData = navigator.userAgentData;
if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {
return uaData.brands.map(function (item) {
return item.brand + "/" + item.version;
}).join(' ');
}
return navigator.userAgent;
}
function isLayoutViewport() {
return !/^((?!chrome|android).)*safari/i.test(getUAString());
}
function getBoundingClientRect(element, includeScale, isFixedStrategy) {
if (includeScale === void 0) {
includeScale = false;
}
if (isFixedStrategy === void 0) {
isFixedStrategy = false;
}
var clientRect = element.getBoundingClientRect();
var scaleX = 1;
var scaleY = 1;
if (includeScale && isHTMLElement(element)) {
scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;
scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;
}
var _ref = isElement(element) ? getWindow(element) : window,
visualViewport = _ref.visualViewport;
var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;
var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;
var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;
var width = clientRect.width / scaleX;
var height = clientRect.height / scaleY;
return {
width: width,
height: height,
top: y,
right: x + width,
bottom: y + height,
left: x,
x: x,
y: y
};
}
function getWindowScroll(node) {
var win = getWindow(node);
var scrollLeft = win.pageXOffset;
var scrollTop = win.pageYOffset;
return {
scrollLeft: scrollLeft,
scrollTop: scrollTop
};
}
function getHTMLElementScroll(element) {
return {
scrollLeft: element.scrollLeft,
scrollTop: element.scrollTop
};
}
function getNodeScroll(node) {
if (node === getWindow(node) || !isHTMLElement(node)) {
return getWindowScroll(node);
} else {
return getHTMLElementScroll(node);
}
}
function getNodeName(element) {
return element ? (element.nodeName || '').toLowerCase() : null;
}
function getDocumentElement(element) {
// $FlowFixMe[incompatible-return]: assume body is always available
return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]
element.document) || window.document).documentElement;
}
function getWindowScrollBarX(element) {
// If <html> has a CSS width greater than the viewport, then this will be
// incorrect for RTL.
// Popper 1 is broken in this case and never had a bug report so let's assume
// it's not an issue. I don't think anyone ever specifies width on <html>
// anyway.
// Browsers where the left scrollbar doesn't cause an issue report `0` for
// this (e.g. Edge 2019, IE11, Safari)
return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;
}
function getComputedStyle(element) {
return getWindow(element).getComputedStyle(element);
}
function isScrollParent(element) {
// Firefox wants us to check `-x` and `-y` variations as well
var _getComputedStyle = getComputedStyle(element),
overflow = _getComputedStyle.overflow,
overflowX = _getComputedStyle.overflowX,
overflowY = _getComputedStyle.overflowY;
return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);
}
function isElementScaled(element) {
var rect = element.getBoundingClientRect();
var scaleX = round(rect.width) / element.offsetWidth || 1;
var scaleY = round(rect.height) / element.offsetHeight || 1;
return scaleX !== 1 || scaleY !== 1;
} // Returns the composite rect of an element relative to its offsetParent.
// Composite means it takes into account transforms as well as layout.
function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {
if (isFixed === void 0) {
isFixed = false;
}
var isOffsetParentAnElement = isHTMLElement(offsetParent);
var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);
var documentElement = getDocumentElement(offsetParent);
var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);
var scroll = {
scrollLeft: 0,
scrollTop: 0
};
var offsets = {
x: 0,
y: 0
};
if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {
if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078
isScrollParent(documentElement)) {
scroll = getNodeScroll(offsetParent);
}
if (isHTMLElement(offsetParent)) {
offsets = getBoundingClientRect(offsetParent, true);
offsets.x += offsetParent.clientLeft;
offsets.y += offsetParent.clientTop;
} else if (documentElement) {
offsets.x = getWindowScrollBarX(documentElement);
}
}
return {
x: rect.left + scroll.scrollLeft - offsets.x,
y: rect.top + scroll.scrollTop - offsets.y,
width: rect.width,
height: rect.height
};
}
// means it doesn't take into account transforms.
function getLayoutRect(element) {
var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.
// Fixes https://github.com/popperjs/popper-core/issues/1223
var width = element.offsetWidth;
var height = element.offsetHeight;
if (Math.abs(clientRect.width - width) <= 1) {
width = clientRect.width;
}
if (Math.abs(clientRect.height - height) <= 1) {
height = clientRect.height;
}
return {
x: element.offsetLeft,
y: element.offsetTop,
width: width,
height: height
};
}
function getParentNode(element) {
if (getNodeName(element) === 'html') {
return element;
}
return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle
// $FlowFixMe[incompatible-return]
// $FlowFixMe[prop-missing]
element.assignedSlot || // step into the shadow DOM of the parent of a slotted node
element.parentNode || ( // DOM Element detected
isShadowRoot(element) ? element.host : null) || // ShadowRoot detected
// $FlowFixMe[incompatible-call]: HTMLElement is a Node
getDocumentElement(element) // fallback
);
}
function getScrollParent(node) {
if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {
// $FlowFixMe[incompatible-return]: assume body is always available
return node.ownerDocument.body;
}
if (isHTMLElement(node) && isScrollParent(node)) {
return node;
}
return getScrollParent(getParentNode(node));
}
/*
given a DOM element, return the list of all scroll parents, up the list of ancesors
until we get to the top window object. This list is what we attach scroll listeners
to, because if any of these parent elements scroll, we'll need to re-calculate the
reference element's position.
*/
function listScrollParents(element, list) {
var _element$ownerDocumen;
if (list === void 0) {
list = [];
}
var scrollParent = getScrollParent(element);
var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);
var win = getWindow(scrollParent);
var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;
var updatedList = list.concat(target);
return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here
updatedList.concat(listScrollParents(getParentNode(target)));
}
function isTableElement(element) {
return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;
}
function getTrueOffsetParent(element) {
if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837
getComputedStyle(element).position === 'fixed') {
return null;
}
return element.offsetParent;
} // `.offsetParent` reports `null` for fixed elements, while absolute elements
// return the containing block
function getContainingBlock(element) {
var isFirefox = /firefox/i.test(getUAString());
var isIE = /Trident/i.test(getUAString());
if (isIE && isHTMLElement(element)) {
// In IE 9, 10 and 11 fixed elements containing block is always established by the viewport
var elementCss = getComputedStyle(element);
if (elementCss.position === 'fixed') {
return null;
}
}
var currentNode = getParentNode(element);
if (isShadowRoot(currentNode)) {
currentNode = currentNode.host;
}
while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {
var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that
// create a containing block.
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {
return currentNode;
} else {
currentNode = currentNode.parentNode;
}
}
return null;
} // Gets the closest ancestor positioned element. Handles some edge cases,
// such as table ancestors and cross browser bugs.
function getOffsetParent(element) {
var window = getWindow(element);
var offsetParent = getTrueOffsetParent(element);
while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {
offsetParent = getTrueOffsetParent(offsetParent);
}
if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {
return window;
}
return offsetParent || getContainingBlock(element) || window;
}
var top = 'top';
var bottom = 'bottom';
var right = 'right';
var left = 'left';
var basePlacements = [top, bottom, right, left];
var start = 'start';
var end = 'end';
var clippingParents = 'clippingParents';
var viewport = 'viewport';
var popper = 'popper';
var reference = 'reference';
var beforeRead = 'beforeRead';
var read = 'read';
var afterRead = 'afterRead'; // pure-logic modifiers
var beforeMain = 'beforeMain';
var main = 'main';
var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)
var beforeWrite = 'beforeWrite';
var write = 'write';
var afterWrite = 'afterWrite';
var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];
function order(modifiers) {
var map = new Map();
var visited = new Set();
var result = [];
modifiers.forEach(function (modifier) {
map.set(modifier.name, modifier);
}); // On visiting object, check for its dependencies and visit them recursively
function sort(modifier) {
visited.add(modifier.name);
var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);
requires.forEach(function (dep) {
if (!visited.has(dep)) {
var depModifier = map.get(dep);
if (depModifier) {
sort(depModifier);
}
}
});
result.push(modifier);
}
modifiers.forEach(function (modifier) {
if (!visited.has(modifier.name)) {
// check for visited object
sort(modifier);
}
});
return result;
}
function orderModifiers(modifiers) {
// order based on dependencies
var orderedModifiers = order(modifiers); // order based on phase
return modifierPhases.reduce(function (acc, phase) {
return acc.concat(orderedModifiers.filter(function (modifier) {
return modifier.phase === phase;
}));
}, []);
}
function debounce(fn) {
var pending;
return function () {
if (!pending) {
pending = new Promise(function (resolve) {
Promise.resolve().then(function () {
pending = undefined;
resolve(fn());
});
});
}
return pending;
};
}
function mergeByName(modifiers) {
var merged = modifiers.reduce(function (merged, current) {
var existing = merged[current.name];
merged[current.name] = existing ? Object.assign({}, existing, current, {
options: Object.assign({}, existing.options, current.options),
data: Object.assign({}, existing.data, current.data)
}) : current;
return merged;
}, {}); // IE11 does not support Object.values
return Object.keys(merged).map(function (key) {
return merged[key];
});
}
function getViewportRect(element, strategy) {
var win = getWindow(element);
var html = getDocumentElement(element);
var visualViewport = win.visualViewport;
var width = html.clientWidth;
var height = html.clientHeight;
var x = 0;
var y = 0;
if (visualViewport) {
width = visualViewport.width;
height = visualViewport.height;
var layoutViewport = isLayoutViewport();
if (layoutViewport || !layoutViewport && strategy === 'fixed') {
x = visualViewport.offsetLeft;
y = visualViewport.offsetTop;
}
}
return {
width: width,
height: height,
x: x + getWindowScrollBarX(element),
y: y
};
}
// of the `<html>` and `<body>` rect bounds if horizontally scrollable
function getDocumentRect(element) {
var _element$ownerDocumen;
var html = getDocumentElement(element);
var winScroll = getWindowScroll(element);
var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;
var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);
var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);
var x = -winScroll.scrollLeft + getWindowScrollBarX(element);
var y = -winScroll.scrollTop;
if (getComputedStyle(body || html).direction === 'rtl') {
x += max(html.clientWidth, body ? body.clientWidth : 0) - width;
}
return {
width: width,
height: height,
x: x,
y: y
};
}
function contains(parent, child) {
var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method
if (parent.contains(child)) {
return true;
} // then fallback to custom implementation with Shadow DOM support
else if (rootNode && isShadowRoot(rootNode)) {
var next = child;
do {
if (next && parent.isSameNode(next)) {
return true;
} // $FlowFixMe[prop-missing]: need a better way to handle this...
next = next.parentNode || next.host;
} while (next);
} // Give up, the result is false
return false;
}
function rectToClientRect(rect) {
return Object.assign({}, rect, {
left: rect.x,
top: rect.y,
right: rect.x + rect.width,
bottom: rect.y + rect.height
});
}
function getInnerBoundingClientRect(element, strategy) {
var rect = getBoundingClientRect(element, false, strategy === 'fixed');
rect.top = rect.top + element.clientTop;
rect.left = rect.left + element.clientLeft;
rect.bottom = rect.top + element.clientHeight;
rect.right = rect.left + element.clientWidth;
rect.width = element.clientWidth;
rect.height = element.clientHeight;
rect.x = rect.left;
rect.y = rect.top;
return rect;
}
function getClientRectFromMixedType(element, clippingParent, strategy) {
return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));
} // A "clipping parent" is an overflowable container with the characteristic of
// clipping (or hiding) overflowing elements with a position different from
// `initial`
function getClippingParents(element) {
var clippingParents = listScrollParents(getParentNode(element));
var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;
var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;
if (!isElement(clipperElement)) {
return [];
} // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414
return clippingParents.filter(function (clippingParent) {
return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';
});
} // Gets the maximum area that the element is visible in due to any number of
// clipping parents
function getClippingRect(element, boundary, rootBoundary, strategy) {
var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);
var clippingParents = [].concat(mainClippingParents, [rootBoundary]);
var firstClippingParent = clippingParents[0];
var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {
var rect = getClientRectFromMixedType(element, clippingParent, strategy);
accRect.top = max(rect.top, accRect.top);
accRect.right = min(rect.right, accRect.right);
accRect.bottom = min(rect.bottom, accRect.bottom);
accRect.left = max(rect.left, accRect.left);
return accRect;
}, getClientRectFromMixedType(element, firstClippingParent, strategy));
clippingRect.width = clippingRect.right - clippingRect.left;
clippingRect.height = clippingRect.bottom - clippingRect.top;
clippingRect.x = clippingRect.left;
clippingRect.y = clippingRect.top;
return clippingRect;
}
function getBasePlacement(placement) {
return placement.split('-')[0];
}
function getVariation(placement) {
return placement.split('-')[1];
}
function getMainAxisFromPlacement(placement) {
return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';
}
function computeOffsets(_ref) {
var reference = _ref.reference,
element = _ref.element,
placement = _ref.placement;
var basePlacement = placement ? getBasePlacement(placement) : null;
var variation = placement ? getVariation(placement) : null;
var commonX = reference.x + reference.width / 2 - element.width / 2;
var commonY = reference.y + reference.height / 2 - element.height / 2;
var offsets;
switch (basePlacement) {
case top:
offsets = {
x: commonX,
y: reference.y - element.height
};
break;
case bottom:
offsets = {
x: commonX,
y: reference.y + reference.height
};
break;
case right:
offsets = {
x: reference.x + reference.width,
y: commonY
};
break;
case left:
offsets = {
x: reference.x - element.width,
y: commonY
};
break;
default:
offsets = {
x: reference.x,
y: reference.y
};
}
var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;
if (mainAxis != null) {
var len = mainAxis === 'y' ? 'height' : 'width';
switch (variation) {
case start:
offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);
break;
case end:
offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);
break;
}
}
return offsets;
}
function getFreshSideObject() {
return {
top: 0,
right: 0,
bottom: 0,
left: 0
};
}
function mergePaddingObject(paddingObject) {
return Object.assign({}, getFreshSideObject(), paddingObject);
}
function expandToHashMap(value, keys) {
return keys.reduce(function (hashMap, key) {
hashMap[key] = value;
return hashMap;
}, {});
}
function detectOverflow(state, options) {
if (options === void 0) {
options = {};
}
var _options = options,
_options$placement = _options.placement,
placement = _options$placement === void 0 ? state.placement : _options$placement,
_options$strategy = _options.strategy,
strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,
_options$boundary = _options.boundary,
boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,
_options$rootBoundary = _options.rootBoundary,
rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,
_options$elementConte = _options.elementContext,
elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,
_options$altBoundary = _options.altBoundary,
altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,
_options$padding = _options.padding,
padding = _options$padding === void 0 ? 0 : _options$padding;
var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));
var altContext = elementContext === popper ? reference : popper;
var popperRect = state.rects.popper;
var element = state.elements[altBoundary ? altContext : elementContext];
var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);
var referenceClientRect = getBoundingClientRect(state.elements.reference);
var popperOffsets = computeOffsets({
reference: referenceClientRect,
element: popperRect,
strategy: 'absolute',
placement: placement
});
var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));
var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect
// 0 or negative = within the clipping rect
var overflowOffsets = {
top: clippingClientRect.top - elementClientRect.top + paddingObject.top,
bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,
left: clippingClientRect.left - elementClientRect.left + paddingObject.left,
right: elementClientRect.right - clippingClientRect.right + paddingObject.right
};
var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element
if (elementContext === popper && offsetData) {
var offset = offsetData[placement];
Object.keys(overflowOffsets).forEach(function (key) {
var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;
var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';
overflowOffsets[key] += offset[axis] * multiply;
});
}
return overflowOffsets;
}
var DEFAULT_OPTIONS = {
placement: 'bottom',
modifiers: [],
strategy: 'absolute'
};
function areValidElements() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return !args.some(function (element) {
return !(element && typeof element.getBoundingClientRect === 'function');
});
}
function popperGenerator(generatorOptions) {
if (generatorOptions === void 0) {
generatorOptions = {};
}
var _generatorOptions = generatorOptions,
_generatorOptions$def = _generatorOptions.defaultModifiers,
defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,
_generatorOptions$def2 = _generatorOptions.defaultOptions,
defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;
return function createPopper(reference, popper, options) {
if (options === void 0) {
options = defaultOptions;
}
var state = {
placement: 'bottom',
orderedModifiers: [],
options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),
modifiersData: {},
elements: {
reference: reference,
popper: popper
},
attributes: {},
styles: {}
};
var effectCleanupFns = [];
var isDestroyed = false;
var instance = {
state: state,
setOptions: function setOptions(setOptionsAction) {
var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;
cleanupModifierEffects();
state.options = Object.assign({}, defaultOptions, state.options, options);
state.scrollParents = {
reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],
popper: listScrollParents(popper)
}; // Orders the modifiers based on their dependencies and `phase`
// properties
var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers
state.orderedModifiers = orderedModifiers.filter(function (m) {
return m.enabled;
});
runModifierEffects();
return instance.update();
},
// Sync update it will always be executed, even if not necessary. This
// is useful for low frequency updates where sync behavior simplifies the
// logic.
// For high frequency updates (e.g. `resize` and `scroll` events), always
// prefer the async Popper#update method
forceUpdate: function forceUpdate() {
if (isDestroyed) {
return;
}
var _state$elements = state.elements,
reference = _state$elements.reference,
popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements
// anymore
if (!areValidElements(reference, popper)) {
return;
} // Store the reference and popper rects to be read by modifiers
state.rects = {
reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),
popper: getLayoutRect(popper)
}; // Modifiers have the ability to reset the current update cycle. The
// most common use case for this is the `flip` modifier changing the
// placement, which then needs to re-run all the modifiers, because the
// logic was previously ran for the previous placement and is therefore
// stale/incorrect
state.reset = false;
state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier
// is filled with the initial data specified by the modifier. This means
// it doesn't persist and is fresh on each update.
// To ensure persistent data, use `${name}#persistent`
state.orderedModifiers.forEach(function (modifier) {
return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);
});
for (var index = 0; index < state.orderedModifiers.length; index++) {
if (state.reset === true) {
state.reset = false;
index = -1;
continue;
}
var _state$orderedModifie = state.orderedModifiers[index],
fn = _state$orderedModifie.fn,
_state$orderedModifie2 = _state$orderedModifie.options,
_options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,
name = _state$orderedModifie.name;
if (typeof fn === 'function') {
state = fn({
state: state,
options: _options,
name: name,
instance: instance
}) || state;
}
}
},
// Async and optimistically optimized update it will not be executed if
// not necessary (debounced to run at most once-per-tick)
update: debounce(function () {
return new Promise(function (resolve) {
instance.forceUpdate();
resolve(state);
});
}),
destroy: function destroy() {
cleanupModifierEffects();
isDestroyed = true;
}
};
if (!areValidElements(reference, popper)) {
return instance;
}
instance.setOptions(options).then(function (state) {
if (!isDestroyed && options.onFirstUpdate) {
options.onFirstUpdate(state);
}
}); // Modifiers have the ability to execute arbitrary code before the first
// update cycle runs. They will be executed in the same order as the update
// cycle. This is useful when a modifier adds some persistent data that
// other modifiers need to use, but the modifier is run after the dependent
// one.
function runModifierEffects() {
state.orderedModifiers.forEach(function (_ref) {
var name = _ref.name,
_ref$options = _ref.options,
options = _ref$options === void 0 ? {} : _ref$options,
effect = _ref.effect;
if (typeof effect === 'function') {
var cleanupFn = effect({
state: state,
name: name,
instance: instance,
options: options
});
var noopFn = function noopFn() {};
effectCleanupFns.push(cleanupFn || noopFn);
}
});
}
function cleanupModifierEffects() {
effectCleanupFns.forEach(function (fn) {
return fn();
});
effectCleanupFns = [];
}
return instance;
};
}
var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules
exports.createPopper = createPopper;
exports.detectOverflow = detectOverflow;
exports.popperGenerator = popperGenerator;
//# sourceMappingURL=popper-base.js.map

View File

@@ -0,0 +1,3 @@
// @flow
export * from '../../lib/popper-base.js'

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
// @flow
export * from '../../lib/popper-lite.js'

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
// @flow
export * from '../../lib/popper.js'

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,199 @@
import getCompositeRect from "./dom-utils/getCompositeRect.js";
import getLayoutRect from "./dom-utils/getLayoutRect.js";
import listScrollParents from "./dom-utils/listScrollParents.js";
import getOffsetParent from "./dom-utils/getOffsetParent.js";
import orderModifiers from "./utils/orderModifiers.js";
import debounce from "./utils/debounce.js";
import mergeByName from "./utils/mergeByName.js";
import detectOverflow from "./utils/detectOverflow.js";
import { isElement } from "./dom-utils/instanceOf.js";
var DEFAULT_OPTIONS = {
placement: 'bottom',
modifiers: [],
strategy: 'absolute'
};
function areValidElements() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return !args.some(function (element) {
return !(element && typeof element.getBoundingClientRect === 'function');
});
}
export function popperGenerator(generatorOptions) {
if (generatorOptions === void 0) {
generatorOptions = {};
}
var _generatorOptions = generatorOptions,
_generatorOptions$def = _generatorOptions.defaultModifiers,
defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,
_generatorOptions$def2 = _generatorOptions.defaultOptions,
defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;
return function createPopper(reference, popper, options) {
if (options === void 0) {
options = defaultOptions;
}
var state = {
placement: 'bottom',
orderedModifiers: [],
options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),
modifiersData: {},
elements: {
reference: reference,
popper: popper
},
attributes: {},
styles: {}
};
var effectCleanupFns = [];
var isDestroyed = false;
var instance = {
state: state,
setOptions: function setOptions(setOptionsAction) {
var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;
cleanupModifierEffects();
state.options = Object.assign({}, defaultOptions, state.options, options);
state.scrollParents = {
reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],
popper: listScrollParents(popper)
}; // Orders the modifiers based on their dependencies and `phase`
// properties
var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers
state.orderedModifiers = orderedModifiers.filter(function (m) {
return m.enabled;
});
runModifierEffects();
return instance.update();
},
// Sync update it will always be executed, even if not necessary. This
// is useful for low frequency updates where sync behavior simplifies the
// logic.
// For high frequency updates (e.g. `resize` and `scroll` events), always
// prefer the async Popper#update method
forceUpdate: function forceUpdate() {
if (isDestroyed) {
return;
}
var _state$elements = state.elements,
reference = _state$elements.reference,
popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements
// anymore
if (!areValidElements(reference, popper)) {
return;
} // Store the reference and popper rects to be read by modifiers
state.rects = {
reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),
popper: getLayoutRect(popper)
}; // Modifiers have the ability to reset the current update cycle. The
// most common use case for this is the `flip` modifier changing the
// placement, which then needs to re-run all the modifiers, because the
// logic was previously ran for the previous placement and is therefore
// stale/incorrect
state.reset = false;
state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier
// is filled with the initial data specified by the modifier. This means
// it doesn't persist and is fresh on each update.
// To ensure persistent data, use `${name}#persistent`
state.orderedModifiers.forEach(function (modifier) {
return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);
});
for (var index = 0; index < state.orderedModifiers.length; index++) {
if (state.reset === true) {
state.reset = false;
index = -1;
continue;
}
var _state$orderedModifie = state.orderedModifiers[index],
fn = _state$orderedModifie.fn,
_state$orderedModifie2 = _state$orderedModifie.options,
_options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,
name = _state$orderedModifie.name;
if (typeof fn === 'function') {
state = fn({
state: state,
options: _options,
name: name,
instance: instance
}) || state;
}
}
},
// Async and optimistically optimized update it will not be executed if
// not necessary (debounced to run at most once-per-tick)
update: debounce(function () {
return new Promise(function (resolve) {
instance.forceUpdate();
resolve(state);
});
}),
destroy: function destroy() {
cleanupModifierEffects();
isDestroyed = true;
}
};
if (!areValidElements(reference, popper)) {
return instance;
}
instance.setOptions(options).then(function (state) {
if (!isDestroyed && options.onFirstUpdate) {
options.onFirstUpdate(state);
}
}); // Modifiers have the ability to execute arbitrary code before the first
// update cycle runs. They will be executed in the same order as the update
// cycle. This is useful when a modifier adds some persistent data that
// other modifiers need to use, but the modifier is run after the dependent
// one.
function runModifierEffects() {
state.orderedModifiers.forEach(function (_ref) {
var name = _ref.name,
_ref$options = _ref.options,
options = _ref$options === void 0 ? {} : _ref$options,
effect = _ref.effect;
if (typeof effect === 'function') {
var cleanupFn = effect({
state: state,
name: name,
instance: instance,
options: options
});
var noopFn = function noopFn() {};
effectCleanupFns.push(cleanupFn || noopFn);
}
});
}
function cleanupModifierEffects() {
effectCleanupFns.forEach(function (fn) {
return fn();
});
effectCleanupFns = [];
}
return instance;
};
}
export var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules
export { detectOverflow };

View File

@@ -0,0 +1,23 @@
import { isShadowRoot } from "./instanceOf.js";
export default function contains(parent, child) {
var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method
if (parent.contains(child)) {
return true;
} // then fallback to custom implementation with Shadow DOM support
else if (rootNode && isShadowRoot(rootNode)) {
var next = child;
do {
if (next && parent.isSameNode(next)) {
return true;
} // $FlowFixMe[prop-missing]: need a better way to handle this...
next = next.parentNode || next.host;
} while (next);
} // Give up, the result is false
return false;
}

View File

@@ -0,0 +1,41 @@
import { isElement, isHTMLElement } from "./instanceOf.js";
import { round } from "../utils/math.js";
import getWindow from "./getWindow.js";
import isLayoutViewport from "./isLayoutViewport.js";
export default function getBoundingClientRect(element, includeScale, isFixedStrategy) {
if (includeScale === void 0) {
includeScale = false;
}
if (isFixedStrategy === void 0) {
isFixedStrategy = false;
}
var clientRect = element.getBoundingClientRect();
var scaleX = 1;
var scaleY = 1;
if (includeScale && isHTMLElement(element)) {
scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;
scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;
}
var _ref = isElement(element) ? getWindow(element) : window,
visualViewport = _ref.visualViewport;
var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;
var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;
var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;
var width = clientRect.width / scaleX;
var height = clientRect.height / scaleY;
return {
width: width,
height: height,
top: y,
right: x + width,
bottom: y + height,
left: x,
x: x,
y: y
};
}

View File

@@ -0,0 +1,70 @@
import { viewport } from "../enums.js";
import getViewportRect from "./getViewportRect.js";
import getDocumentRect from "./getDocumentRect.js";
import listScrollParents from "./listScrollParents.js";
import getOffsetParent from "./getOffsetParent.js";
import getDocumentElement from "./getDocumentElement.js";
import getComputedStyle from "./getComputedStyle.js";
import { isElement, isHTMLElement } from "./instanceOf.js";
import getBoundingClientRect from "./getBoundingClientRect.js";
import getParentNode from "./getParentNode.js";
import contains from "./contains.js";
import getNodeName from "./getNodeName.js";
import rectToClientRect from "../utils/rectToClientRect.js";
import { max, min } from "../utils/math.js";
function getInnerBoundingClientRect(element, strategy) {
var rect = getBoundingClientRect(element, false, strategy === 'fixed');
rect.top = rect.top + element.clientTop;
rect.left = rect.left + element.clientLeft;
rect.bottom = rect.top + element.clientHeight;
rect.right = rect.left + element.clientWidth;
rect.width = element.clientWidth;
rect.height = element.clientHeight;
rect.x = rect.left;
rect.y = rect.top;
return rect;
}
function getClientRectFromMixedType(element, clippingParent, strategy) {
return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));
} // A "clipping parent" is an overflowable container with the characteristic of
// clipping (or hiding) overflowing elements with a position different from
// `initial`
function getClippingParents(element) {
var clippingParents = listScrollParents(getParentNode(element));
var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;
var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;
if (!isElement(clipperElement)) {
return [];
} // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414
return clippingParents.filter(function (clippingParent) {
return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';
});
} // Gets the maximum area that the element is visible in due to any number of
// clipping parents
export default function getClippingRect(element, boundary, rootBoundary, strategy) {
var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);
var clippingParents = [].concat(mainClippingParents, [rootBoundary]);
var firstClippingParent = clippingParents[0];
var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {
var rect = getClientRectFromMixedType(element, clippingParent, strategy);
accRect.top = max(rect.top, accRect.top);
accRect.right = min(rect.right, accRect.right);
accRect.bottom = min(rect.bottom, accRect.bottom);
accRect.left = max(rect.left, accRect.left);
return accRect;
}, getClientRectFromMixedType(element, firstClippingParent, strategy));
clippingRect.width = clippingRect.right - clippingRect.left;
clippingRect.height = clippingRect.bottom - clippingRect.top;
clippingRect.x = clippingRect.left;
clippingRect.y = clippingRect.top;
return clippingRect;
}

View File

@@ -0,0 +1,58 @@
import getBoundingClientRect from "./getBoundingClientRect.js";
import getNodeScroll from "./getNodeScroll.js";
import getNodeName from "./getNodeName.js";
import { isHTMLElement } from "./instanceOf.js";
import getWindowScrollBarX from "./getWindowScrollBarX.js";
import getDocumentElement from "./getDocumentElement.js";
import isScrollParent from "./isScrollParent.js";
import { round } from "../utils/math.js";
function isElementScaled(element) {
var rect = element.getBoundingClientRect();
var scaleX = round(rect.width) / element.offsetWidth || 1;
var scaleY = round(rect.height) / element.offsetHeight || 1;
return scaleX !== 1 || scaleY !== 1;
} // Returns the composite rect of an element relative to its offsetParent.
// Composite means it takes into account transforms as well as layout.
export default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {
if (isFixed === void 0) {
isFixed = false;
}
var isOffsetParentAnElement = isHTMLElement(offsetParent);
var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);
var documentElement = getDocumentElement(offsetParent);
var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);
var scroll = {
scrollLeft: 0,
scrollTop: 0
};
var offsets = {
x: 0,
y: 0
};
if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {
if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078
isScrollParent(documentElement)) {
scroll = getNodeScroll(offsetParent);
}
if (isHTMLElement(offsetParent)) {
offsets = getBoundingClientRect(offsetParent, true);
offsets.x += offsetParent.clientLeft;
offsets.y += offsetParent.clientTop;
} else if (documentElement) {
offsets.x = getWindowScrollBarX(documentElement);
}
}
return {
x: rect.left + scroll.scrollLeft - offsets.x,
y: rect.top + scroll.scrollTop - offsets.y,
width: rect.width,
height: rect.height
};
}

View File

@@ -0,0 +1,4 @@
import getWindow from "./getWindow.js";
export default function getComputedStyle(element) {
return getWindow(element).getComputedStyle(element);
}

View File

@@ -0,0 +1,6 @@
import { isElement } from "./instanceOf.js";
export default function getDocumentElement(element) {
// $FlowFixMe[incompatible-return]: assume body is always available
return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]
element.document) || window.document).documentElement;
}

View File

@@ -0,0 +1,29 @@
import getDocumentElement from "./getDocumentElement.js";
import getComputedStyle from "./getComputedStyle.js";
import getWindowScrollBarX from "./getWindowScrollBarX.js";
import getWindowScroll from "./getWindowScroll.js";
import { max } from "../utils/math.js"; // Gets the entire size of the scrollable document area, even extending outside
// of the `<html>` and `<body>` rect bounds if horizontally scrollable
export default function getDocumentRect(element) {
var _element$ownerDocumen;
var html = getDocumentElement(element);
var winScroll = getWindowScroll(element);
var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;
var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);
var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);
var x = -winScroll.scrollLeft + getWindowScrollBarX(element);
var y = -winScroll.scrollTop;
if (getComputedStyle(body || html).direction === 'rtl') {
x += max(html.clientWidth, body ? body.clientWidth : 0) - width;
}
return {
width: width,
height: height,
x: x,
y: y
};
}

View File

@@ -0,0 +1,6 @@
export default function getHTMLElementScroll(element) {
return {
scrollLeft: element.scrollLeft,
scrollTop: element.scrollTop
};
}

View File

@@ -0,0 +1,25 @@
import getBoundingClientRect from "./getBoundingClientRect.js"; // Returns the layout rect of an element relative to its offsetParent. Layout
// means it doesn't take into account transforms.
export default function getLayoutRect(element) {
var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.
// Fixes https://github.com/popperjs/popper-core/issues/1223
var width = element.offsetWidth;
var height = element.offsetHeight;
if (Math.abs(clientRect.width - width) <= 1) {
width = clientRect.width;
}
if (Math.abs(clientRect.height - height) <= 1) {
height = clientRect.height;
}
return {
x: element.offsetLeft,
y: element.offsetTop,
width: width,
height: height
};
}

View File

@@ -0,0 +1,3 @@
export default function getNodeName(element) {
return element ? (element.nodeName || '').toLowerCase() : null;
}

View File

@@ -0,0 +1,11 @@
import getWindowScroll from "./getWindowScroll.js";
import getWindow from "./getWindow.js";
import { isHTMLElement } from "./instanceOf.js";
import getHTMLElementScroll from "./getHTMLElementScroll.js";
export default function getNodeScroll(node) {
if (node === getWindow(node) || !isHTMLElement(node)) {
return getWindowScroll(node);
} else {
return getHTMLElementScroll(node);
}
}

View File

@@ -0,0 +1,69 @@
import getWindow from "./getWindow.js";
import getNodeName from "./getNodeName.js";
import getComputedStyle from "./getComputedStyle.js";
import { isHTMLElement, isShadowRoot } from "./instanceOf.js";
import isTableElement from "./isTableElement.js";
import getParentNode from "./getParentNode.js";
import getUAString from "../utils/userAgent.js";
function getTrueOffsetParent(element) {
if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837
getComputedStyle(element).position === 'fixed') {
return null;
}
return element.offsetParent;
} // `.offsetParent` reports `null` for fixed elements, while absolute elements
// return the containing block
function getContainingBlock(element) {
var isFirefox = /firefox/i.test(getUAString());
var isIE = /Trident/i.test(getUAString());
if (isIE && isHTMLElement(element)) {
// In IE 9, 10 and 11 fixed elements containing block is always established by the viewport
var elementCss = getComputedStyle(element);
if (elementCss.position === 'fixed') {
return null;
}
}
var currentNode = getParentNode(element);
if (isShadowRoot(currentNode)) {
currentNode = currentNode.host;
}
while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {
var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that
// create a containing block.
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {
return currentNode;
} else {
currentNode = currentNode.parentNode;
}
}
return null;
} // Gets the closest ancestor positioned element. Handles some edge cases,
// such as table ancestors and cross browser bugs.
export default function getOffsetParent(element) {
var window = getWindow(element);
var offsetParent = getTrueOffsetParent(element);
while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {
offsetParent = getTrueOffsetParent(offsetParent);
}
if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {
return window;
}
return offsetParent || getContainingBlock(element) || window;
}

View File

@@ -0,0 +1,19 @@
import getNodeName from "./getNodeName.js";
import getDocumentElement from "./getDocumentElement.js";
import { isShadowRoot } from "./instanceOf.js";
export default function getParentNode(element) {
if (getNodeName(element) === 'html') {
return element;
}
return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle
// $FlowFixMe[incompatible-return]
// $FlowFixMe[prop-missing]
element.assignedSlot || // step into the shadow DOM of the parent of a slotted node
element.parentNode || ( // DOM Element detected
isShadowRoot(element) ? element.host : null) || // ShadowRoot detected
// $FlowFixMe[incompatible-call]: HTMLElement is a Node
getDocumentElement(element) // fallback
);
}

View File

@@ -0,0 +1,16 @@
import getParentNode from "./getParentNode.js";
import isScrollParent from "./isScrollParent.js";
import getNodeName from "./getNodeName.js";
import { isHTMLElement } from "./instanceOf.js";
export default function getScrollParent(node) {
if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {
// $FlowFixMe[incompatible-return]: assume body is always available
return node.ownerDocument.body;
}
if (isHTMLElement(node) && isScrollParent(node)) {
return node;
}
return getScrollParent(getParentNode(node));
}

View File

@@ -0,0 +1,31 @@
import getWindow from "./getWindow.js";
import getDocumentElement from "./getDocumentElement.js";
import getWindowScrollBarX from "./getWindowScrollBarX.js";
import isLayoutViewport from "./isLayoutViewport.js";
export default function getViewportRect(element, strategy) {
var win = getWindow(element);
var html = getDocumentElement(element);
var visualViewport = win.visualViewport;
var width = html.clientWidth;
var height = html.clientHeight;
var x = 0;
var y = 0;
if (visualViewport) {
width = visualViewport.width;
height = visualViewport.height;
var layoutViewport = isLayoutViewport();
if (layoutViewport || !layoutViewport && strategy === 'fixed') {
x = visualViewport.offsetLeft;
y = visualViewport.offsetTop;
}
}
return {
width: width,
height: height,
x: x + getWindowScrollBarX(element),
y: y
};
}

View File

@@ -0,0 +1,12 @@
export default function getWindow(node) {
if (node == null) {
return window;
}
if (node.toString() !== '[object Window]') {
var ownerDocument = node.ownerDocument;
return ownerDocument ? ownerDocument.defaultView || window : window;
}
return node;
}

View File

@@ -0,0 +1,10 @@
import getWindow from "./getWindow.js";
export default function getWindowScroll(node) {
var win = getWindow(node);
var scrollLeft = win.pageXOffset;
var scrollTop = win.pageYOffset;
return {
scrollLeft: scrollLeft,
scrollTop: scrollTop
};
}

View File

@@ -0,0 +1,13 @@
import getBoundingClientRect from "./getBoundingClientRect.js";
import getDocumentElement from "./getDocumentElement.js";
import getWindowScroll from "./getWindowScroll.js";
export default function getWindowScrollBarX(element) {
// If <html> has a CSS width greater than the viewport, then this will be
// incorrect for RTL.
// Popper 1 is broken in this case and never had a bug report so let's assume
// it's not an issue. I don't think anyone ever specifies width on <html>
// anyway.
// Browsers where the left scrollbar doesn't cause an issue report `0` for
// this (e.g. Edge 2019, IE11, Safari)
return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;
}

View File

@@ -0,0 +1,23 @@
import getWindow from "./getWindow.js";
function isElement(node) {
var OwnElement = getWindow(node).Element;
return node instanceof OwnElement || node instanceof Element;
}
function isHTMLElement(node) {
var OwnElement = getWindow(node).HTMLElement;
return node instanceof OwnElement || node instanceof HTMLElement;
}
function isShadowRoot(node) {
// IE 11 has no ShadowRoot
if (typeof ShadowRoot === 'undefined') {
return false;
}
var OwnElement = getWindow(node).ShadowRoot;
return node instanceof OwnElement || node instanceof ShadowRoot;
}
export { isElement, isHTMLElement, isShadowRoot };

View File

@@ -0,0 +1,4 @@
import getUAString from "../utils/userAgent.js";
export default function isLayoutViewport() {
return !/^((?!chrome|android).)*safari/i.test(getUAString());
}

View File

@@ -0,0 +1,10 @@
import getComputedStyle from "./getComputedStyle.js";
export default function isScrollParent(element) {
// Firefox wants us to check `-x` and `-y` variations as well
var _getComputedStyle = getComputedStyle(element),
overflow = _getComputedStyle.overflow,
overflowX = _getComputedStyle.overflowX,
overflowY = _getComputedStyle.overflowY;
return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);
}

View File

@@ -0,0 +1,4 @@
import getNodeName from "./getNodeName.js";
export default function isTableElement(element) {
return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;
}

View File

@@ -0,0 +1,26 @@
import getScrollParent from "./getScrollParent.js";
import getParentNode from "./getParentNode.js";
import getWindow from "./getWindow.js";
import isScrollParent from "./isScrollParent.js";
/*
given a DOM element, return the list of all scroll parents, up the list of ancesors
until we get to the top window object. This list is what we attach scroll listeners
to, because if any of these parent elements scroll, we'll need to re-calculate the
reference element's position.
*/
export default function listScrollParents(element, list) {
var _element$ownerDocumen;
if (list === void 0) {
list = [];
}
var scrollParent = getScrollParent(element);
var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);
var win = getWindow(scrollParent);
var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;
var updatedList = list.concat(target);
return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here
updatedList.concat(listScrollParents(getParentNode(target)));
}

View File

@@ -0,0 +1,31 @@
export var top = 'top';
export var bottom = 'bottom';
export var right = 'right';
export var left = 'left';
export var auto = 'auto';
export var basePlacements = [top, bottom, right, left];
export var start = 'start';
export var end = 'end';
export var clippingParents = 'clippingParents';
export var viewport = 'viewport';
export var popper = 'popper';
export var reference = 'reference';
export var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {
return acc.concat([placement + "-" + start, placement + "-" + end]);
}, []);
export var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {
return acc.concat([placement, placement + "-" + start, placement + "-" + end]);
}, []); // modifiers that need to read the DOM
export var beforeRead = 'beforeRead';
export var read = 'read';
export var afterRead = 'afterRead'; // pure-logic modifiers
export var beforeMain = 'beforeMain';
export var main = 'main';
export var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)
export var beforeWrite = 'beforeWrite';
export var write = 'write';
export var afterWrite = 'afterWrite';
export var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];

View File

@@ -0,0 +1,8 @@
export * from "./enums.js";
export * from "./modifiers/index.js"; // eslint-disable-next-line import/no-unused-modules
export { popperGenerator, detectOverflow, createPopper as createPopperBase } from "./createPopper.js"; // eslint-disable-next-line import/no-unused-modules
export { createPopper } from "./popper.js"; // eslint-disable-next-line import/no-unused-modules
export { createPopper as createPopperLite } from "./popper-lite.js";

View File

@@ -0,0 +1,84 @@
import getNodeName from "../dom-utils/getNodeName.js";
import { isHTMLElement } from "../dom-utils/instanceOf.js"; // This modifier takes the styles prepared by the `computeStyles` modifier
// and applies them to the HTMLElements such as popper and arrow
function applyStyles(_ref) {
var state = _ref.state;
Object.keys(state.elements).forEach(function (name) {
var style = state.styles[name] || {};
var attributes = state.attributes[name] || {};
var element = state.elements[name]; // arrow is optional + virtual elements
if (!isHTMLElement(element) || !getNodeName(element)) {
return;
} // Flow doesn't support to extend this property, but it's the most
// effective way to apply styles to an HTMLElement
// $FlowFixMe[cannot-write]
Object.assign(element.style, style);
Object.keys(attributes).forEach(function (name) {
var value = attributes[name];
if (value === false) {
element.removeAttribute(name);
} else {
element.setAttribute(name, value === true ? '' : value);
}
});
});
}
function effect(_ref2) {
var state = _ref2.state;
var initialStyles = {
popper: {
position: state.options.strategy,
left: '0',
top: '0',
margin: '0'
},
arrow: {
position: 'absolute'
},
reference: {}
};
Object.assign(state.elements.popper.style, initialStyles.popper);
state.styles = initialStyles;
if (state.elements.arrow) {
Object.assign(state.elements.arrow.style, initialStyles.arrow);
}
return function () {
Object.keys(state.elements).forEach(function (name) {
var element = state.elements[name];
var attributes = state.attributes[name] || {};
var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them
var style = styleProperties.reduce(function (style, property) {
style[property] = '';
return style;
}, {}); // arrow is optional + virtual elements
if (!isHTMLElement(element) || !getNodeName(element)) {
return;
}
Object.assign(element.style, style);
Object.keys(attributes).forEach(function (attribute) {
element.removeAttribute(attribute);
});
});
};
} // eslint-disable-next-line import/no-unused-modules
export default {
name: 'applyStyles',
enabled: true,
phase: 'write',
fn: applyStyles,
effect: effect,
requires: ['computeStyles']
};

View File

@@ -0,0 +1,90 @@
import getBasePlacement from "../utils/getBasePlacement.js";
import getLayoutRect from "../dom-utils/getLayoutRect.js";
import contains from "../dom-utils/contains.js";
import getOffsetParent from "../dom-utils/getOffsetParent.js";
import getMainAxisFromPlacement from "../utils/getMainAxisFromPlacement.js";
import { within } from "../utils/within.js";
import mergePaddingObject from "../utils/mergePaddingObject.js";
import expandToHashMap from "../utils/expandToHashMap.js";
import { left, right, basePlacements, top, bottom } from "../enums.js"; // eslint-disable-next-line import/no-unused-modules
var toPaddingObject = function toPaddingObject(padding, state) {
padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {
placement: state.placement
})) : padding;
return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));
};
function arrow(_ref) {
var _state$modifiersData$;
var state = _ref.state,
name = _ref.name,
options = _ref.options;
var arrowElement = state.elements.arrow;
var popperOffsets = state.modifiersData.popperOffsets;
var basePlacement = getBasePlacement(state.placement);
var axis = getMainAxisFromPlacement(basePlacement);
var isVertical = [left, right].indexOf(basePlacement) >= 0;
var len = isVertical ? 'height' : 'width';
if (!arrowElement || !popperOffsets) {
return;
}
var paddingObject = toPaddingObject(options.padding, state);
var arrowRect = getLayoutRect(arrowElement);
var minProp = axis === 'y' ? top : left;
var maxProp = axis === 'y' ? bottom : right;
var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];
var startDiff = popperOffsets[axis] - state.rects.reference[axis];
var arrowOffsetParent = getOffsetParent(arrowElement);
var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;
var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is
// outside of the popper bounds
var min = paddingObject[minProp];
var max = clientSize - arrowRect[len] - paddingObject[maxProp];
var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;
var offset = within(min, center, max); // Prevents breaking syntax highlighting...
var axisProp = axis;
state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);
}
function effect(_ref2) {
var state = _ref2.state,
options = _ref2.options;
var _options$element = options.element,
arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;
if (arrowElement == null) {
return;
} // CSS selector
if (typeof arrowElement === 'string') {
arrowElement = state.elements.popper.querySelector(arrowElement);
if (!arrowElement) {
return;
}
}
if (!contains(state.elements.popper, arrowElement)) {
return;
}
state.elements.arrow = arrowElement;
} // eslint-disable-next-line import/no-unused-modules
export default {
name: 'arrow',
enabled: true,
phase: 'main',
fn: arrow,
effect: effect,
requires: ['popperOffsets'],
requiresIfExists: ['preventOverflow']
};

View File

@@ -0,0 +1,169 @@
import { top, left, right, bottom, end } from "../enums.js";
import getOffsetParent from "../dom-utils/getOffsetParent.js";
import getWindow from "../dom-utils/getWindow.js";
import getDocumentElement from "../dom-utils/getDocumentElement.js";
import getComputedStyle from "../dom-utils/getComputedStyle.js";
import getBasePlacement from "../utils/getBasePlacement.js";
import getVariation from "../utils/getVariation.js";
import { round } from "../utils/math.js"; // eslint-disable-next-line import/no-unused-modules
var unsetSides = {
top: 'auto',
right: 'auto',
bottom: 'auto',
left: 'auto'
}; // Round the offsets to the nearest suitable subpixel based on the DPR.
// Zooming can change the DPR, but it seems to report a value that will
// cleanly divide the values into the appropriate subpixels.
function roundOffsetsByDPR(_ref, win) {
var x = _ref.x,
y = _ref.y;
var dpr = win.devicePixelRatio || 1;
return {
x: round(x * dpr) / dpr || 0,
y: round(y * dpr) / dpr || 0
};
}
export function mapToStyles(_ref2) {
var _Object$assign2;
var popper = _ref2.popper,
popperRect = _ref2.popperRect,
placement = _ref2.placement,
variation = _ref2.variation,
offsets = _ref2.offsets,
position = _ref2.position,
gpuAcceleration = _ref2.gpuAcceleration,
adaptive = _ref2.adaptive,
roundOffsets = _ref2.roundOffsets,
isFixed = _ref2.isFixed;
var _offsets$x = offsets.x,
x = _offsets$x === void 0 ? 0 : _offsets$x,
_offsets$y = offsets.y,
y = _offsets$y === void 0 ? 0 : _offsets$y;
var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({
x: x,
y: y
}) : {
x: x,
y: y
};
x = _ref3.x;
y = _ref3.y;
var hasX = offsets.hasOwnProperty('x');
var hasY = offsets.hasOwnProperty('y');
var sideX = left;
var sideY = top;
var win = window;
if (adaptive) {
var offsetParent = getOffsetParent(popper);
var heightProp = 'clientHeight';
var widthProp = 'clientWidth';
if (offsetParent === getWindow(popper)) {
offsetParent = getDocumentElement(popper);
if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {
heightProp = 'scrollHeight';
widthProp = 'scrollWidth';
}
} // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it
offsetParent = offsetParent;
if (placement === top || (placement === left || placement === right) && variation === end) {
sideY = bottom;
var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]
offsetParent[heightProp];
y -= offsetY - popperRect.height;
y *= gpuAcceleration ? 1 : -1;
}
if (placement === left || (placement === top || placement === bottom) && variation === end) {
sideX = right;
var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]
offsetParent[widthProp];
x -= offsetX - popperRect.width;
x *= gpuAcceleration ? 1 : -1;
}
}
var commonStyles = Object.assign({
position: position
}, adaptive && unsetSides);
var _ref4 = roundOffsets === true ? roundOffsetsByDPR({
x: x,
y: y
}, getWindow(popper)) : {
x: x,
y: y
};
x = _ref4.x;
y = _ref4.y;
if (gpuAcceleration) {
var _Object$assign;
return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? "translate(" + x + "px, " + y + "px)" : "translate3d(" + x + "px, " + y + "px, 0)", _Object$assign));
}
return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + "px" : '', _Object$assign2[sideX] = hasX ? x + "px" : '', _Object$assign2.transform = '', _Object$assign2));
}
function computeStyles(_ref5) {
var state = _ref5.state,
options = _ref5.options;
var _options$gpuAccelerat = options.gpuAcceleration,
gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,
_options$adaptive = options.adaptive,
adaptive = _options$adaptive === void 0 ? true : _options$adaptive,
_options$roundOffsets = options.roundOffsets,
roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;
var commonStyles = {
placement: getBasePlacement(state.placement),
variation: getVariation(state.placement),
popper: state.elements.popper,
popperRect: state.rects.popper,
gpuAcceleration: gpuAcceleration,
isFixed: state.options.strategy === 'fixed'
};
if (state.modifiersData.popperOffsets != null) {
state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {
offsets: state.modifiersData.popperOffsets,
position: state.options.strategy,
adaptive: adaptive,
roundOffsets: roundOffsets
})));
}
if (state.modifiersData.arrow != null) {
state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {
offsets: state.modifiersData.arrow,
position: 'absolute',
adaptive: false,
roundOffsets: roundOffsets
})));
}
state.attributes.popper = Object.assign({}, state.attributes.popper, {
'data-popper-placement': state.placement
});
} // eslint-disable-next-line import/no-unused-modules
export default {
name: 'computeStyles',
enabled: true,
phase: 'beforeWrite',
fn: computeStyles,
data: {}
};

View File

@@ -0,0 +1,49 @@
import getWindow from "../dom-utils/getWindow.js"; // eslint-disable-next-line import/no-unused-modules
var passive = {
passive: true
};
function effect(_ref) {
var state = _ref.state,
instance = _ref.instance,
options = _ref.options;
var _options$scroll = options.scroll,
scroll = _options$scroll === void 0 ? true : _options$scroll,
_options$resize = options.resize,
resize = _options$resize === void 0 ? true : _options$resize;
var window = getWindow(state.elements.popper);
var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);
if (scroll) {
scrollParents.forEach(function (scrollParent) {
scrollParent.addEventListener('scroll', instance.update, passive);
});
}
if (resize) {
window.addEventListener('resize', instance.update, passive);
}
return function () {
if (scroll) {
scrollParents.forEach(function (scrollParent) {
scrollParent.removeEventListener('scroll', instance.update, passive);
});
}
if (resize) {
window.removeEventListener('resize', instance.update, passive);
}
};
} // eslint-disable-next-line import/no-unused-modules
export default {
name: 'eventListeners',
enabled: true,
phase: 'write',
fn: function fn() {},
effect: effect,
data: {}
};

View File

@@ -0,0 +1,147 @@
import getOppositePlacement from "../utils/getOppositePlacement.js";
import getBasePlacement from "../utils/getBasePlacement.js";
import getOppositeVariationPlacement from "../utils/getOppositeVariationPlacement.js";
import detectOverflow from "../utils/detectOverflow.js";
import computeAutoPlacement from "../utils/computeAutoPlacement.js";
import { bottom, top, start, right, left, auto } from "../enums.js";
import getVariation from "../utils/getVariation.js"; // eslint-disable-next-line import/no-unused-modules
function getExpandedFallbackPlacements(placement) {
if (getBasePlacement(placement) === auto) {
return [];
}
var oppositePlacement = getOppositePlacement(placement);
return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];
}
function flip(_ref) {
var state = _ref.state,
options = _ref.options,
name = _ref.name;
if (state.modifiersData[name]._skip) {
return;
}
var _options$mainAxis = options.mainAxis,
checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,
_options$altAxis = options.altAxis,
checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,
specifiedFallbackPlacements = options.fallbackPlacements,
padding = options.padding,
boundary = options.boundary,
rootBoundary = options.rootBoundary,
altBoundary = options.altBoundary,
_options$flipVariatio = options.flipVariations,
flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,
allowedAutoPlacements = options.allowedAutoPlacements;
var preferredPlacement = state.options.placement;
var basePlacement = getBasePlacement(preferredPlacement);
var isBasePlacement = basePlacement === preferredPlacement;
var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));
var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {
return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {
placement: placement,
boundary: boundary,
rootBoundary: rootBoundary,
padding: padding,
flipVariations: flipVariations,
allowedAutoPlacements: allowedAutoPlacements
}) : placement);
}, []);
var referenceRect = state.rects.reference;
var popperRect = state.rects.popper;
var checksMap = new Map();
var makeFallbackChecks = true;
var firstFittingPlacement = placements[0];
for (var i = 0; i < placements.length; i++) {
var placement = placements[i];
var _basePlacement = getBasePlacement(placement);
var isStartVariation = getVariation(placement) === start;
var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;
var len = isVertical ? 'width' : 'height';
var overflow = detectOverflow(state, {
placement: placement,
boundary: boundary,
rootBoundary: rootBoundary,
altBoundary: altBoundary,
padding: padding
});
var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;
if (referenceRect[len] > popperRect[len]) {
mainVariationSide = getOppositePlacement(mainVariationSide);
}
var altVariationSide = getOppositePlacement(mainVariationSide);
var checks = [];
if (checkMainAxis) {
checks.push(overflow[_basePlacement] <= 0);
}
if (checkAltAxis) {
checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);
}
if (checks.every(function (check) {
return check;
})) {
firstFittingPlacement = placement;
makeFallbackChecks = false;
break;
}
checksMap.set(placement, checks);
}
if (makeFallbackChecks) {
// `2` may be desired in some cases research later
var numberOfChecks = flipVariations ? 3 : 1;
var _loop = function _loop(_i) {
var fittingPlacement = placements.find(function (placement) {
var checks = checksMap.get(placement);
if (checks) {
return checks.slice(0, _i).every(function (check) {
return check;
});
}
});
if (fittingPlacement) {
firstFittingPlacement = fittingPlacement;
return "break";
}
};
for (var _i = numberOfChecks; _i > 0; _i--) {
var _ret = _loop(_i);
if (_ret === "break") break;
}
}
if (state.placement !== firstFittingPlacement) {
state.modifiersData[name]._skip = true;
state.placement = firstFittingPlacement;
state.reset = true;
}
} // eslint-disable-next-line import/no-unused-modules
export default {
name: 'flip',
enabled: true,
phase: 'main',
fn: flip,
requiresIfExists: ['offset'],
data: {
_skip: false
}
};

View File

@@ -0,0 +1,61 @@
import { top, bottom, left, right } from "../enums.js";
import detectOverflow from "../utils/detectOverflow.js";
function getSideOffsets(overflow, rect, preventedOffsets) {
if (preventedOffsets === void 0) {
preventedOffsets = {
x: 0,
y: 0
};
}
return {
top: overflow.top - rect.height - preventedOffsets.y,
right: overflow.right - rect.width + preventedOffsets.x,
bottom: overflow.bottom - rect.height + preventedOffsets.y,
left: overflow.left - rect.width - preventedOffsets.x
};
}
function isAnySideFullyClipped(overflow) {
return [top, right, bottom, left].some(function (side) {
return overflow[side] >= 0;
});
}
function hide(_ref) {
var state = _ref.state,
name = _ref.name;
var referenceRect = state.rects.reference;
var popperRect = state.rects.popper;
var preventedOffsets = state.modifiersData.preventOverflow;
var referenceOverflow = detectOverflow(state, {
elementContext: 'reference'
});
var popperAltOverflow = detectOverflow(state, {
altBoundary: true
});
var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);
var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);
var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);
var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);
state.modifiersData[name] = {
referenceClippingOffsets: referenceClippingOffsets,
popperEscapeOffsets: popperEscapeOffsets,
isReferenceHidden: isReferenceHidden,
hasPopperEscaped: hasPopperEscaped
};
state.attributes.popper = Object.assign({}, state.attributes.popper, {
'data-popper-reference-hidden': isReferenceHidden,
'data-popper-escaped': hasPopperEscaped
});
} // eslint-disable-next-line import/no-unused-modules
export default {
name: 'hide',
enabled: true,
phase: 'main',
requiresIfExists: ['preventOverflow'],
fn: hide
};

View File

@@ -0,0 +1,9 @@
export { default as applyStyles } from "./applyStyles.js";
export { default as arrow } from "./arrow.js";
export { default as computeStyles } from "./computeStyles.js";
export { default as eventListeners } from "./eventListeners.js";
export { default as flip } from "./flip.js";
export { default as hide } from "./hide.js";
export { default as offset } from "./offset.js";
export { default as popperOffsets } from "./popperOffsets.js";
export { default as preventOverflow } from "./preventOverflow.js";

View File

@@ -0,0 +1,54 @@
import getBasePlacement from "../utils/getBasePlacement.js";
import { top, left, right, placements } from "../enums.js"; // eslint-disable-next-line import/no-unused-modules
export function distanceAndSkiddingToXY(placement, rects, offset) {
var basePlacement = getBasePlacement(placement);
var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;
var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {
placement: placement
})) : offset,
skidding = _ref[0],
distance = _ref[1];
skidding = skidding || 0;
distance = (distance || 0) * invertDistance;
return [left, right].indexOf(basePlacement) >= 0 ? {
x: distance,
y: skidding
} : {
x: skidding,
y: distance
};
}
function offset(_ref2) {
var state = _ref2.state,
options = _ref2.options,
name = _ref2.name;
var _options$offset = options.offset,
offset = _options$offset === void 0 ? [0, 0] : _options$offset;
var data = placements.reduce(function (acc, placement) {
acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);
return acc;
}, {});
var _data$state$placement = data[state.placement],
x = _data$state$placement.x,
y = _data$state$placement.y;
if (state.modifiersData.popperOffsets != null) {
state.modifiersData.popperOffsets.x += x;
state.modifiersData.popperOffsets.y += y;
}
state.modifiersData[name] = data;
} // eslint-disable-next-line import/no-unused-modules
export default {
name: 'offset',
enabled: true,
phase: 'main',
requires: ['popperOffsets'],
fn: offset
};

View File

@@ -0,0 +1,25 @@
import computeOffsets from "../utils/computeOffsets.js";
function popperOffsets(_ref) {
var state = _ref.state,
name = _ref.name;
// Offsets are the actual position the popper needs to have to be
// properly positioned near its reference element
// This is the most basic placement, and will be adjusted by
// the modifiers in the next step
state.modifiersData[name] = computeOffsets({
reference: state.rects.reference,
element: state.rects.popper,
strategy: 'absolute',
placement: state.placement
});
} // eslint-disable-next-line import/no-unused-modules
export default {
name: 'popperOffsets',
enabled: true,
phase: 'read',
fn: popperOffsets,
data: {}
};

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