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