first commit
This commit is contained in:
58
.air.toml
Normal file
58
.air.toml
Normal file
@@ -0,0 +1,58 @@
|
||||
#:schema https://json.schemastore.org/any.json
|
||||
|
||||
env_files = []
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 1000
|
||||
entrypoint = ["./tmp/main"]
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
ignore_dangerous_root_dir = false
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
silent = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[proxy]
|
||||
app_port = 0
|
||||
app_start_timeout = 0
|
||||
enabled = false
|
||||
proxy_port = 0
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
32
.env
Normal file
32
.env
Normal 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
31
.env.copy
Normal 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
29
.gitignore
vendored
Normal 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
|
||||
9
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV
Normal file
9
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV
Normal file
@@ -0,0 +1,9 @@
|
||||
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2LCJlbWFpbCI6ImJleWhhbkBiZXloYW4uZGV2IiwiaXNfYWRtaW4iOnRydWUsImZpcnN0X25hbWUiOiJCZXloYW4iLCJsYXN0X25hbWUiOiJPxJ91ciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJzdWIiOiI2IiwiZXhwIjoxNzcxNzAyMTU2LCJpYXQiOjE3NzE2OTQ5NTZ9.QHid2xqKsdwe1E-vkrZLA7nB_qL3DEcEWztbkFoOaZU
|
||||
|
||||
|
||||
|
||||
|
||||
a1a4e309bb7b1ea86c5c046a22a5d5e4f7ec727a
|
||||
|
||||
|
||||
81f4318fe76b595c582aa9f2baf26818894303d0
|
||||
71
Dockerfile
Normal file
71
Dockerfile
Normal 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
96
belgeler/admin_panel.md
Normal 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
119
config/config.go
Normal 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
39
config/loger.go
Normal 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)")
|
||||
}
|
||||
52
controllers/admin_cart_controller.go
Normal file
52
controllers/admin_cart_controller.go
Normal 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")
|
||||
}
|
||||
1641
controllers/admin_controller.go
Normal file
1641
controllers/admin_controller.go
Normal file
File diff suppressed because it is too large
Load Diff
619
controllers/admin_product_controller.go
Normal file
619
controllers/admin_product_controller.go
Normal 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")
|
||||
}
|
||||
237
controllers/api_cart_controller.go
Normal file
237
controllers/api_cart_controller.go
Normal 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)
|
||||
}
|
||||
1123
controllers/blog_controller.go
Normal file
1123
controllers/blog_controller.go
Normal file
File diff suppressed because it is too large
Load Diff
164
controllers/hero_controller.go
Normal file
164
controllers/hero_controller.go
Normal 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"})
|
||||
}
|
||||
161
controllers/product_controller.go
Normal file
161
controllers/product_controller.go
Normal 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)
|
||||
}
|
||||
419
controllers/security_controller.go
Normal file
419
controllers/security_controller.go
Normal 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...)
|
||||
}
|
||||
}
|
||||
}
|
||||
150
controllers/setting_controller.go
Normal file
150
controllers/setting_controller.go
Normal 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
798
controllers/user.go
Normal 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
|
||||
}
|
||||
46
database/config/mysql_db.go
Normal file
46
database/config/mysql_db.go
Normal 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
|
||||
}
|
||||
46
database/config/postgres_db.go
Normal file
46
database/config/postgres_db.go
Normal 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
117
database/config/redis_db.go
Normal 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
162
database/migrate/migrate.go
Normal 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
53
database/models/blog.go
Normal 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
20
database/models/cart.go
Normal 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
34
database/models/cors.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CorsWhitelist - CORS için izin verilen origin'ler
|
||||
type CorsWhitelist struct {
|
||||
gorm.Model
|
||||
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
|
||||
Description string `gorm:"type:varchar(255)" json:"description"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
// CorsBlacklist - CORS için yasaklanan origin'ler
|
||||
type CorsBlacklist struct {
|
||||
gorm.Model
|
||||
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
|
||||
Reason string `gorm:"type:varchar(255)" json:"reason"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
// RateLimitSetting - Rate limit ayarları
|
||||
type RateLimitSetting struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"type:varchar(100);uniqueIndex;not null" json:"name"` // e.g., "login", "register", "api"
|
||||
Description string `gorm:"type:varchar(255)" json:"description"`
|
||||
MaxRequests int64 `gorm:"not null" json:"max_requests"` // Max istek sayısı
|
||||
WindowSeconds int `gorm:"not null" json:"window_seconds"` // Zaman penceresi (saniye)
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
UpdatedBy string `gorm:"type:varchar(255)" json:"updated_by,omitempty"`
|
||||
}
|
||||
96
database/models/docs_models.go
Normal file
96
database/models/docs_models.go
Normal 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
23
database/models/hero.go
Normal 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"`
|
||||
}
|
||||
53
database/models/product.go
Normal file
53
database/models/product.go
Normal 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"`
|
||||
}
|
||||
43
database/models/setting.go
Normal file
43
database/models/setting.go
Normal 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
48
database/models/user.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
UserName string `json:"username" gorm:"type:varchar(255)"`
|
||||
Email string `gorm:"uniqueIndex;not null;type:varchar(255)" json:"email"`
|
||||
Password string `json:"-" gorm:"type:varchar(255)"` // Password shouldn't be returned in JSON
|
||||
EmailVerified *bool `gorm:"default:false" json:"email_verified"` // default false for email/password registration
|
||||
EmailVerifyToken string `gorm:"index;type:varchar(255)" json:"-"`
|
||||
EmailVerifiedAt *time.Time `json:"email_verified_at,omitempty"`
|
||||
IsAdmin *bool `gorm:"default:false" json:"is_admin"`
|
||||
SocialAccounts []SocialAccount `gorm:"foreignKey:UserID" json:"social_accounts,omitempty"`
|
||||
Profile []Profile `gorm:"foreignKey:UserID" json:"profiles,omitempty"`
|
||||
}
|
||||
|
||||
// Email Veriyf i False Döndürüyor
|
||||
func (u *User) IsEmailVerified() bool {
|
||||
if u.EmailVerified == nil {
|
||||
return false
|
||||
}
|
||||
return *u.EmailVerified
|
||||
}
|
||||
|
||||
// SocialAccount model structure
|
||||
type SocialAccount struct {
|
||||
gorm.Model
|
||||
UserID uint64 `gorm:"type:bigint unsigned;not null;index" json:"user_id"`
|
||||
Provider string `gorm:"not null" json:"provider"` // google, github
|
||||
ProviderID string `gorm:"not null" json:"provider_id"`
|
||||
Email string `json:"email" gorm:"type:varchar(255)"`
|
||||
Name string `json:"name,omitempty" gorm:"type:varchar(255)"` // Full name from provider
|
||||
AvatarURL string `json:"avatar_url,omitempty" gorm:"type:varchar(255)"` // Avatar URL from provider
|
||||
|
||||
}
|
||||
type Profile struct {
|
||||
gorm.Model
|
||||
UserID uint64 `gorm:"type:bigint unsigned;not null;index" json:"user_id"`
|
||||
AvatarURL string `json:"avatar_url,omitempty" gorm:"type:varchar(255)"` // Avatar URL from provider
|
||||
FirstName string `json:"first_name" gorm:"type:varchar(255)"` // Full name from provider
|
||||
LastName string `json:"last_name" gorm:"type:varchar(255)"` // Full name from provider
|
||||
|
||||
}
|
||||
75
database/seeder/seeder.go
Normal file
75
database/seeder/seeder.go
Normal 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
20
docker-compose.c.yml
Normal 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
26
docker-compose.yml
Normal 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
13
docker-entrypoint.sh
Normal 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
1377
docs/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
1348
docs/swagger.json
Normal file
1348
docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
872
docs/swagger.yaml
Normal file
872
docs/swagger.yaml
Normal 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
70
go.mod
Normal 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
172
go.sum
Normal 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
122
info.log
Normal 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
130
main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
108
middlewares/auth_middleware.go
Normal file
108
middlewares/auth_middleware.go
Normal 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
151
middlewares/dynamic_cors.go
Normal 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
136
middlewares/rate_limit.go
Normal 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...)
|
||||
}
|
||||
}
|
||||
}
|
||||
12
middlewares/reject_all_middleware.go
Normal file
12
middlewares/reject_all_middleware.go
Normal 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
48
pkg/utis/slug.go
Normal 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
15
pkg/utis/token.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// GenerateSecureToken returns a cryptographically random hex string (e.g. for email verification).
|
||||
func GenerateSecureToken(byteLength int) (string, error) {
|
||||
b := make([]byte, byteLength)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
146
public/admin/css/theme.css
Normal file
146
public/admin/css/theme.css
Normal 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
63
public/admin/js/main.js
Normal 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
77
public/assets/.package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
20
public/assets/@popperjs/core/LICENSE.md
Normal file
20
public/assets/@popperjs/core/LICENSE.md
Normal 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.
|
||||
376
public/assets/@popperjs/core/README.md
Normal file
376
public/assets/@popperjs/core/README.md
Normal 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
|
||||
|
||||
[](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 -->
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## 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
|
||||
65
public/assets/@popperjs/core/dist/cjs/enums.js
vendored
Normal file
65
public/assets/@popperjs/core/dist/cjs/enums.js
vendored
Normal 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
|
||||
3
public/assets/@popperjs/core/dist/cjs/enums.js.flow
vendored
Normal file
3
public/assets/@popperjs/core/dist/cjs/enums.js.flow
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from '../../lib/enums.js'
|
||||
1
public/assets/@popperjs/core/dist/cjs/enums.js.map
vendored
Normal file
1
public/assets/@popperjs/core/dist/cjs/enums.js.map
vendored
Normal 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;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
||||
939
public/assets/@popperjs/core/dist/cjs/popper-base.js
vendored
Normal file
939
public/assets/@popperjs/core/dist/cjs/popper-base.js
vendored
Normal 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
|
||||
3
public/assets/@popperjs/core/dist/cjs/popper-base.js.flow
vendored
Normal file
3
public/assets/@popperjs/core/dist/cjs/popper-base.js.flow
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from '../../lib/popper-base.js'
|
||||
1
public/assets/@popperjs/core/dist/cjs/popper-base.js.map
vendored
Normal file
1
public/assets/@popperjs/core/dist/cjs/popper-base.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1260
public/assets/@popperjs/core/dist/cjs/popper-lite.js
vendored
Normal file
1260
public/assets/@popperjs/core/dist/cjs/popper-lite.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
public/assets/@popperjs/core/dist/cjs/popper-lite.js.flow
vendored
Normal file
3
public/assets/@popperjs/core/dist/cjs/popper-lite.js.flow
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from '../../lib/popper-lite.js'
|
||||
1
public/assets/@popperjs/core/dist/cjs/popper-lite.js.map
vendored
Normal file
1
public/assets/@popperjs/core/dist/cjs/popper-lite.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1819
public/assets/@popperjs/core/dist/cjs/popper.js
vendored
Normal file
1819
public/assets/@popperjs/core/dist/cjs/popper.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
public/assets/@popperjs/core/dist/cjs/popper.js.flow
vendored
Normal file
3
public/assets/@popperjs/core/dist/cjs/popper.js.flow
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from '../../lib/popper.js'
|
||||
1
public/assets/@popperjs/core/dist/cjs/popper.js.map
vendored
Normal file
1
public/assets/@popperjs/core/dist/cjs/popper.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
199
public/assets/@popperjs/core/dist/esm/createPopper.js
vendored
Normal file
199
public/assets/@popperjs/core/dist/esm/createPopper.js
vendored
Normal 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 };
|
||||
23
public/assets/@popperjs/core/dist/esm/dom-utils/contains.js
vendored
Normal file
23
public/assets/@popperjs/core/dist/esm/dom-utils/contains.js
vendored
Normal 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;
|
||||
}
|
||||
41
public/assets/@popperjs/core/dist/esm/dom-utils/getBoundingClientRect.js
vendored
Normal file
41
public/assets/@popperjs/core/dist/esm/dom-utils/getBoundingClientRect.js
vendored
Normal 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
|
||||
};
|
||||
}
|
||||
70
public/assets/@popperjs/core/dist/esm/dom-utils/getClippingRect.js
vendored
Normal file
70
public/assets/@popperjs/core/dist/esm/dom-utils/getClippingRect.js
vendored
Normal 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;
|
||||
}
|
||||
58
public/assets/@popperjs/core/dist/esm/dom-utils/getCompositeRect.js
vendored
Normal file
58
public/assets/@popperjs/core/dist/esm/dom-utils/getCompositeRect.js
vendored
Normal 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
|
||||
};
|
||||
}
|
||||
4
public/assets/@popperjs/core/dist/esm/dom-utils/getComputedStyle.js
vendored
Normal file
4
public/assets/@popperjs/core/dist/esm/dom-utils/getComputedStyle.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import getWindow from "./getWindow.js";
|
||||
export default function getComputedStyle(element) {
|
||||
return getWindow(element).getComputedStyle(element);
|
||||
}
|
||||
6
public/assets/@popperjs/core/dist/esm/dom-utils/getDocumentElement.js
vendored
Normal file
6
public/assets/@popperjs/core/dist/esm/dom-utils/getDocumentElement.js
vendored
Normal 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;
|
||||
}
|
||||
29
public/assets/@popperjs/core/dist/esm/dom-utils/getDocumentRect.js
vendored
Normal file
29
public/assets/@popperjs/core/dist/esm/dom-utils/getDocumentRect.js
vendored
Normal 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
|
||||
};
|
||||
}
|
||||
6
public/assets/@popperjs/core/dist/esm/dom-utils/getHTMLElementScroll.js
vendored
Normal file
6
public/assets/@popperjs/core/dist/esm/dom-utils/getHTMLElementScroll.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function getHTMLElementScroll(element) {
|
||||
return {
|
||||
scrollLeft: element.scrollLeft,
|
||||
scrollTop: element.scrollTop
|
||||
};
|
||||
}
|
||||
25
public/assets/@popperjs/core/dist/esm/dom-utils/getLayoutRect.js
vendored
Normal file
25
public/assets/@popperjs/core/dist/esm/dom-utils/getLayoutRect.js
vendored
Normal 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
|
||||
};
|
||||
}
|
||||
3
public/assets/@popperjs/core/dist/esm/dom-utils/getNodeName.js
vendored
Normal file
3
public/assets/@popperjs/core/dist/esm/dom-utils/getNodeName.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function getNodeName(element) {
|
||||
return element ? (element.nodeName || '').toLowerCase() : null;
|
||||
}
|
||||
11
public/assets/@popperjs/core/dist/esm/dom-utils/getNodeScroll.js
vendored
Normal file
11
public/assets/@popperjs/core/dist/esm/dom-utils/getNodeScroll.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
69
public/assets/@popperjs/core/dist/esm/dom-utils/getOffsetParent.js
vendored
Normal file
69
public/assets/@popperjs/core/dist/esm/dom-utils/getOffsetParent.js
vendored
Normal 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;
|
||||
}
|
||||
19
public/assets/@popperjs/core/dist/esm/dom-utils/getParentNode.js
vendored
Normal file
19
public/assets/@popperjs/core/dist/esm/dom-utils/getParentNode.js
vendored
Normal 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
|
||||
|
||||
);
|
||||
}
|
||||
16
public/assets/@popperjs/core/dist/esm/dom-utils/getScrollParent.js
vendored
Normal file
16
public/assets/@popperjs/core/dist/esm/dom-utils/getScrollParent.js
vendored
Normal 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));
|
||||
}
|
||||
31
public/assets/@popperjs/core/dist/esm/dom-utils/getViewportRect.js
vendored
Normal file
31
public/assets/@popperjs/core/dist/esm/dom-utils/getViewportRect.js
vendored
Normal 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
|
||||
};
|
||||
}
|
||||
12
public/assets/@popperjs/core/dist/esm/dom-utils/getWindow.js
vendored
Normal file
12
public/assets/@popperjs/core/dist/esm/dom-utils/getWindow.js
vendored
Normal 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;
|
||||
}
|
||||
10
public/assets/@popperjs/core/dist/esm/dom-utils/getWindowScroll.js
vendored
Normal file
10
public/assets/@popperjs/core/dist/esm/dom-utils/getWindowScroll.js
vendored
Normal 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
|
||||
};
|
||||
}
|
||||
13
public/assets/@popperjs/core/dist/esm/dom-utils/getWindowScrollBarX.js
vendored
Normal file
13
public/assets/@popperjs/core/dist/esm/dom-utils/getWindowScrollBarX.js
vendored
Normal 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;
|
||||
}
|
||||
23
public/assets/@popperjs/core/dist/esm/dom-utils/instanceOf.js
vendored
Normal file
23
public/assets/@popperjs/core/dist/esm/dom-utils/instanceOf.js
vendored
Normal 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 };
|
||||
4
public/assets/@popperjs/core/dist/esm/dom-utils/isLayoutViewport.js
vendored
Normal file
4
public/assets/@popperjs/core/dist/esm/dom-utils/isLayoutViewport.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import getUAString from "../utils/userAgent.js";
|
||||
export default function isLayoutViewport() {
|
||||
return !/^((?!chrome|android).)*safari/i.test(getUAString());
|
||||
}
|
||||
10
public/assets/@popperjs/core/dist/esm/dom-utils/isScrollParent.js
vendored
Normal file
10
public/assets/@popperjs/core/dist/esm/dom-utils/isScrollParent.js
vendored
Normal 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);
|
||||
}
|
||||
4
public/assets/@popperjs/core/dist/esm/dom-utils/isTableElement.js
vendored
Normal file
4
public/assets/@popperjs/core/dist/esm/dom-utils/isTableElement.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import getNodeName from "./getNodeName.js";
|
||||
export default function isTableElement(element) {
|
||||
return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;
|
||||
}
|
||||
26
public/assets/@popperjs/core/dist/esm/dom-utils/listScrollParents.js
vendored
Normal file
26
public/assets/@popperjs/core/dist/esm/dom-utils/listScrollParents.js
vendored
Normal 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)));
|
||||
}
|
||||
31
public/assets/@popperjs/core/dist/esm/enums.js
vendored
Normal file
31
public/assets/@popperjs/core/dist/esm/enums.js
vendored
Normal 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];
|
||||
8
public/assets/@popperjs/core/dist/esm/index.js
vendored
Normal file
8
public/assets/@popperjs/core/dist/esm/index.js
vendored
Normal 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";
|
||||
84
public/assets/@popperjs/core/dist/esm/modifiers/applyStyles.js
vendored
Normal file
84
public/assets/@popperjs/core/dist/esm/modifiers/applyStyles.js
vendored
Normal 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']
|
||||
};
|
||||
90
public/assets/@popperjs/core/dist/esm/modifiers/arrow.js
vendored
Normal file
90
public/assets/@popperjs/core/dist/esm/modifiers/arrow.js
vendored
Normal 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']
|
||||
};
|
||||
169
public/assets/@popperjs/core/dist/esm/modifiers/computeStyles.js
vendored
Normal file
169
public/assets/@popperjs/core/dist/esm/modifiers/computeStyles.js
vendored
Normal 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: {}
|
||||
};
|
||||
49
public/assets/@popperjs/core/dist/esm/modifiers/eventListeners.js
vendored
Normal file
49
public/assets/@popperjs/core/dist/esm/modifiers/eventListeners.js
vendored
Normal 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: {}
|
||||
};
|
||||
147
public/assets/@popperjs/core/dist/esm/modifiers/flip.js
vendored
Normal file
147
public/assets/@popperjs/core/dist/esm/modifiers/flip.js
vendored
Normal 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
|
||||
}
|
||||
};
|
||||
61
public/assets/@popperjs/core/dist/esm/modifiers/hide.js
vendored
Normal file
61
public/assets/@popperjs/core/dist/esm/modifiers/hide.js
vendored
Normal 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
|
||||
};
|
||||
9
public/assets/@popperjs/core/dist/esm/modifiers/index.js
vendored
Normal file
9
public/assets/@popperjs/core/dist/esm/modifiers/index.js
vendored
Normal 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";
|
||||
54
public/assets/@popperjs/core/dist/esm/modifiers/offset.js
vendored
Normal file
54
public/assets/@popperjs/core/dist/esm/modifiers/offset.js
vendored
Normal 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
|
||||
};
|
||||
25
public/assets/@popperjs/core/dist/esm/modifiers/popperOffsets.js
vendored
Normal file
25
public/assets/@popperjs/core/dist/esm/modifiers/popperOffsets.js
vendored
Normal 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
Reference in New Issue
Block a user