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","public"]
|
||||||
|
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
|
||||||
40
.env
Normal file
40
.env
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
### 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
|
||||||
|
################################
|
||||||
|
# Harici Resim İşleme API
|
||||||
|
#IMAGE_API_URL=https://v2.beyhano.com.tr
|
||||||
|
#IMAGE_API_URL=https://v3.beyhan.gen.tr
|
||||||
|
IMAGE_API_URL=http://localhost:3000
|
||||||
|
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'
|
||||||
|
IMAGE_API=https://v2.beyhano.com.tr/api/v1/image/upload
|
||||||
|
IMAGE_USERNAME=goares
|
||||||
|
IMAGE_API_KEY=img_44HbL9V1B9RSADJze8MmY6fgc9Z_-T1P
|
||||||
36
.env.copy
Normal file
36
.env.copy
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
### 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
|
||||||
|
################################
|
||||||
|
# Harici Resim İşleme API
|
||||||
|
IMAGE_API_URL=https://v2.beyhano.com.tr
|
||||||
|
IMAGE_API_EMAIL=beyhan@beyhan.dev
|
||||||
|
IMAGE_API_PASSWORD=1923btO**
|
||||||
|
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
|
||||||
54
Dockerfile
Normal file
54
Dockerfile
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Builder: pure Go, no CGO needed (external image API)
|
||||||
|
FROM golang:1.26.0-bookworm AS builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
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 not required)
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
|
||||||
|
|
||||||
|
# --- Final Stage ---
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
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.
|
||||||
50
build_windows.sh
Executable file
50
build_windows.sh
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Renk tanımlamaları
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # Renk Yok
|
||||||
|
|
||||||
|
echo -e "${BLUE}=== Go Windows Build Script ===${NC}"
|
||||||
|
|
||||||
|
# Çıktı klasörünü oluştur
|
||||||
|
OUTPUT_DIR="builds"
|
||||||
|
mkdir -p $OUTPUT_DIR
|
||||||
|
|
||||||
|
# Ana dosya adı (main.go varsayılır, yoksa ilk argümanı al)
|
||||||
|
ENTRY_FILE=${1:-"main.go"}
|
||||||
|
APP_NAME=$(basename $(pwd))
|
||||||
|
|
||||||
|
if [ ! -f "$ENTRY_FILE" ]; then
|
||||||
|
echo -e "${RED}Hata: $ENTRY_FILE bulunamadı!${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "Derleniyor: ${GREEN}$ENTRY_FILE${NC}"
|
||||||
|
|
||||||
|
# Mimari seçimi (Varsayılan amd64)
|
||||||
|
read -p "Mimari seçin (1: amd64 [Varsayılan], 2: 386): " ARCH_CHOICE
|
||||||
|
|
||||||
|
if [[ "$ARCH_CHOICE" == "2" ]]; then
|
||||||
|
export GOARCH=386
|
||||||
|
FINAL_NAME="${APP_NAME}_x86.exe"
|
||||||
|
else
|
||||||
|
export GOARCH=amd64
|
||||||
|
FINAL_NAME="${APP_NAME}_x64.exe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Derleme işlemi
|
||||||
|
# -s -w bayrakları dosya boyutunu ciddi oranda küçültür
|
||||||
|
echo -e "${BLUE}Windows için derleniyor...${NC}"
|
||||||
|
|
||||||
|
env GOOS=windows CGO_ENABLED=0 \
|
||||||
|
go build -ldflags="-s -w" \
|
||||||
|
-o "$OUTPUT_DIR/$FINAL_NAME" "$ENTRY_FILE"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}Başarılı!${NC}"
|
||||||
|
echo -e "Çıktı: ${BLUE}$OUTPUT_DIR/$FINAL_NAME${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Derleme sırasında bir hata oluştu.${NC}"
|
||||||
|
fi
|
||||||
127
config/config.go
Normal file
127
config/config.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
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
|
||||||
|
ImageAPIURL string // Harici resim işleme servisi
|
||||||
|
ImageAPIKey string
|
||||||
|
ImageAPIEmail string
|
||||||
|
ImageAPIPassword string
|
||||||
|
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),
|
||||||
|
ImageAPIURL: getEnv("IMAGE_API_URL", "https://v2.beyhano.com.tr"),
|
||||||
|
ImageAPIKey: getEnv("IMAGE_API_KEY", ""),
|
||||||
|
ImageAPIEmail: getEnv("IMAGE_API_EMAIL", ""),
|
||||||
|
ImageAPIPassword: getEnv("IMAGE_API_PASSWORD", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
1735
controllers/admin_controller.go
Normal file
1735
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...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
166
controllers/setting_controller.go
Normal file
166
controllers/setting_controller.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
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"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"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 {
|
||||||
|
configs.Logger.Info(
|
||||||
|
"CreateSetting called",
|
||||||
|
zap.String("method", c.Method()),
|
||||||
|
zap.String("path", c.Path()),
|
||||||
|
zap.String("content_type", c.Get("Content-Type")),
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
configs.Logger.Info(
|
||||||
|
"UpdateSetting called",
|
||||||
|
zap.String("method", c.Method()),
|
||||||
|
zap.String("path", c.Path()),
|
||||||
|
zap.String("content_type", c.Get("Content-Type")),
|
||||||
|
)
|
||||||
|
|
||||||
|
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"})
|
||||||
|
}
|
||||||
974
controllers/user.go
Normal file
974
controllers/user.go
Normal file
@@ -0,0 +1,974 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
configs "ares/config"
|
||||||
|
database "ares/database/config"
|
||||||
|
"ares/database/models"
|
||||||
|
"ares/middlewares"
|
||||||
|
utils "ares/pkg/utis"
|
||||||
|
"crypto/sha256"
|
||||||
|
"ares/services"
|
||||||
|
"encoding/hex"
|
||||||
|
"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"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist refresh token server-side for rotation & revoke
|
||||||
|
refreshClaims, err := jwtService.ValidateToken(refreshToken)
|
||||||
|
if err != nil || refreshClaims.TokenType != services.TokenTypeRefresh {
|
||||||
|
if configs.Logger != nil {
|
||||||
|
configs.Logger.Sugar().Errorf("failed to validate issued refresh token for user=%d: %v", user.ID, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Revoke any existing refresh tokens for this user (single-device semantics)
|
||||||
|
if err := database.DB.Model(&models.RefreshToken{}).
|
||||||
|
Where("user_id = ?", user.ID).
|
||||||
|
Update("revoked", true).Error; err != nil {
|
||||||
|
if configs.Logger != nil {
|
||||||
|
configs.Logger.Sugar().Errorf("failed to revoke existing refresh tokens for user=%d: %v", user.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresAt := time.Now().Add(time.Duration(configs.AppConfig.RefreshTokenExpireDays) * 24 * time.Hour)
|
||||||
|
if refreshClaims.ExpiresAt != nil {
|
||||||
|
expiresAt = refreshClaims.ExpiresAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := models.RefreshToken{
|
||||||
|
UserID: user.ID,
|
||||||
|
TokenID: refreshClaims.ID,
|
||||||
|
TokenHash: sha256Hex(refreshToken),
|
||||||
|
TokenFingerprint: tokenFingerprint(refreshToken),
|
||||||
|
ExpiresAt: expiresAt.UTC(),
|
||||||
|
Revoked: false,
|
||||||
|
UserAgent: c.Get("User-Agent"),
|
||||||
|
IP: c.IP(),
|
||||||
|
}
|
||||||
|
if err := database.DB.Create(&rt).Error; err != nil {
|
||||||
|
if configs.Logger != nil {
|
||||||
|
configs.Logger.Sugar().Errorf("failed to persist refresh token for user=%d: %v", user.ID, err)
|
||||||
|
}
|
||||||
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "refresh token could not be persisted"})
|
||||||
|
}
|
||||||
|
if configs.Logger != nil {
|
||||||
|
configs.Logger.Sugar().Infof("refresh token persisted user=%d token_id=%s fingerprint=%s", user.ID, rt.TokenID, rt.TokenFingerprint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up refresh token server-side to enforce rotation and revoke state
|
||||||
|
var stored models.RefreshToken
|
||||||
|
if err := database.DB.Where("token_id = ? AND user_id = ?", claims.ID, claims.UserID).First(&stored).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid or expired refresh token"})
|
||||||
|
}
|
||||||
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "refresh token lookup failed"})
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
if stored.Revoked || stored.ExpiresAt.Before(now) {
|
||||||
|
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid or expired refresh token"})
|
||||||
|
}
|
||||||
|
// Extra safety: if we have a stored hash, require it to match.
|
||||||
|
if stored.TokenHash != "" && stored.TokenHash != sha256Hex(req.RefreshToken) {
|
||||||
|
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid refresh token"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse detection: if this token was already rotated to a new one, treat as suspicious
|
||||||
|
if stored.ReplacedByTokenID != "" {
|
||||||
|
if configs.Logger != nil {
|
||||||
|
configs.Logger.Sugar().Warnf("refresh token reuse detected for user=%d token_id=%s", claims.UserID, claims.ID)
|
||||||
|
}
|
||||||
|
// Revoke all refresh tokens for this user to force re-login
|
||||||
|
if err := database.DB.Model(&models.RefreshToken{}).
|
||||||
|
Where("user_id = ?", claims.UserID).
|
||||||
|
Update("revoked", true).Error; err != nil {
|
||||||
|
if configs.Logger != nil {
|
||||||
|
configs.Logger.Sugar().Errorf("failed to revoke refresh tokens after reuse for user=%d: %v", claims.UserID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "refresh token has been reused; please login again"})
|
||||||
|
}
|
||||||
|
|
||||||
|
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"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist new refresh token and rotate old one
|
||||||
|
newClaims, err := jwtService.ValidateToken(refreshToken)
|
||||||
|
if err != nil || newClaims.TokenType != services.TokenTypeRefresh {
|
||||||
|
if configs.Logger != nil {
|
||||||
|
configs.Logger.Sugar().Errorf("failed to validate rotated refresh token for user=%d: %v", user.ID, err)
|
||||||
|
}
|
||||||
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "refresh token rotation failed"})
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresAt := now.Add(time.Duration(configs.AppConfig.RefreshTokenExpireDays) * 24 * time.Hour)
|
||||||
|
if newClaims.ExpiresAt != nil {
|
||||||
|
expiresAt = newClaims.ExpiresAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Model(&stored).Updates(map[string]interface{}{
|
||||||
|
"revoked": true,
|
||||||
|
"replaced_by_token_id": newClaims.ID,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := models.RefreshToken{
|
||||||
|
UserID: user.ID,
|
||||||
|
TokenID: newClaims.ID,
|
||||||
|
TokenHash: sha256Hex(refreshToken),
|
||||||
|
TokenFingerprint: tokenFingerprint(refreshToken),
|
||||||
|
ExpiresAt: expiresAt.UTC(),
|
||||||
|
Revoked: false,
|
||||||
|
UserAgent: c.Get("User-Agent"),
|
||||||
|
IP: c.IP(),
|
||||||
|
}
|
||||||
|
if err := tx.Create(&rt).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
if configs.Logger != nil {
|
||||||
|
configs.Logger.Sugar().Errorf("failed to rotate refresh token for user=%d: %v", user.ID, err)
|
||||||
|
}
|
||||||
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "refresh token rotation failed"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"access_token": accessToken,
|
||||||
|
"refresh_token": refreshToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256Hex(s string) string {
|
||||||
|
sum := sha256.Sum256([]byte(s))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenFingerprint(token string) string {
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if len(token) <= 10 {
|
||||||
|
return "****"
|
||||||
|
}
|
||||||
|
return token[:6] + "..." + token[len(token)-4:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout godoc
|
||||||
|
// @Summary Logout user and revoke refresh tokens
|
||||||
|
// @Tags Auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body RefreshRequest true "Logout payload (refresh token)"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 401 {object} map[string]string
|
||||||
|
// @Router /api/v1/auth/logout [post]
|
||||||
|
func Logout(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"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revoke all refresh tokens for this user to enforce full logout
|
||||||
|
if err := database.DB.Model(&models.RefreshToken{}).
|
||||||
|
Where("user_id = ?", claims.UserID).
|
||||||
|
Update("revoked", true).Error; err != nil {
|
||||||
|
if configs.Logger != nil {
|
||||||
|
configs.Logger.Sugar().Errorf("failed to revoke refresh tokens on logout for user=%d: %v", claims.UserID, err)
|
||||||
|
}
|
||||||
|
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "logout failed"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{"message": "logged out successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
165
database/migrate/migrate.go
Normal file
165
database/migrate/migrate.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
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.RefreshToken{},
|
||||||
|
&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.RefreshToken{},
|
||||||
|
&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",
|
||||||
|
"http://localhost:8000",
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
27
database/models/token.go
Normal file
27
database/models/token.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RefreshToken represents a server-side record of issued refresh tokens
|
||||||
|
// to support rotation, revocation and reuse detection.
|
||||||
|
type RefreshToken struct {
|
||||||
|
gorm.Model
|
||||||
|
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||||
|
TokenID string `gorm:"type:varchar(128);not null;uniqueIndex" json:"token_id"`
|
||||||
|
// TokenHash is SHA-256 hex of the refresh token string (64 chars).
|
||||||
|
// Stored instead of the raw token for security, while still allowing debug/lookup.
|
||||||
|
TokenHash string `gorm:"type:char(64);index" json:"token_hash"`
|
||||||
|
// TokenFingerprint is a masked representation (e.g. first6...last4) to help operators
|
||||||
|
// visually correlate DB rows with logs without storing full token.
|
||||||
|
TokenFingerprint string `gorm:"type:varchar(32);index" json:"token_fingerprint"`
|
||||||
|
ExpiresAt time.Time `gorm:"index" json:"expires_at"`
|
||||||
|
Revoked bool `gorm:"index" json:"revoked"`
|
||||||
|
ReplacedByTokenID string `gorm:"type:varchar(128)" json:"replaced_by_token_id"`
|
||||||
|
UserAgent string `gorm:"type:varchar(255)" json:"user_agent"`
|
||||||
|
IP string `gorm:"type:varchar(64)" json:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
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_v2
|
||||||
|
# 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_v2
|
||||||
|
# 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"
|
||||||
2
git config --global user.log
Normal file
2
git config --global user.log
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
git config --global user.email "beyhano@gmail.com"
|
||||||
|
git config --global user.name "Beyhan Oğur"
|
||||||
69
go.mod
Normal file
69
go.mod
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
module ares
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-playground/validator/v10 v10.30.2
|
||||||
|
github.com/gofiber/fiber/v3 v3.1.0
|
||||||
|
github.com/gofiber/template/html/v2 v2.1.3
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
|
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.49.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.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.5 // indirect
|
||||||
|
github.com/go-openapi/spec v0.22.4 // indirect
|
||||||
|
github.com/go-openapi/swag/conv v0.25.5 // indirect
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
|
||||||
|
github.com/go-openapi/swag/loading v0.25.5 // indirect
|
||||||
|
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
|
||||||
|
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.25.5 // 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.9.1 // 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.5 // 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.34.0 // indirect
|
||||||
|
golang.org/x/net v0.52.0 // indirect
|
||||||
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
golang.org/x/text v0.35.0 // indirect
|
||||||
|
golang.org/x/tools v0.43.0 // indirect
|
||||||
|
)
|
||||||
170
go.sum
Normal file
170
go.sum
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
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.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||||
|
github.com/andybalholm/brotli v1.2.1/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.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
|
||||||
|
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
|
||||||
|
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
|
||||||
|
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||||
|
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
|
||||||
|
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
|
||||||
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
|
||||||
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
|
||||||
|
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
|
||||||
|
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
|
||||||
|
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
|
||||||
|
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
|
||||||
|
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
|
||||||
|
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
|
||||||
|
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
|
||||||
|
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
|
||||||
|
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
|
||||||
|
github.com/go-openapi/testify/v2 v2.4.0/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.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||||
|
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.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=
|
||||||
|
github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=
|
||||||
|
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/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.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
|
||||||
|
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
|
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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||||
|
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
|
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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
|
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=
|
||||||
125
guvenlik-raporu.md
Normal file
125
guvenlik-raporu.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
## Go Backend API Güvenlik Raporu
|
||||||
|
|
||||||
|
### 1. Genel Değerlendirme
|
||||||
|
|
||||||
|
- **Kapsam**: Kod tabanı üzerinden statik güvenlik analizi ve `go vet ./...` ile temel statik araç kontrolü.
|
||||||
|
- **Genel Sonuç**: Mimari olarak sağlam bir temel var (JWT, rol tabanlı yetki, rate limit, CORS). En önemli eksik, **refresh token güvenliğinin state-less olması (rotation/revoke yok)** ve **API tarafında token invalidation / logout akışının olmaması**.
|
||||||
|
|
||||||
|
### 2. Güçlü Yönler
|
||||||
|
|
||||||
|
- **JWT ve Kimlik Doğrulama**
|
||||||
|
- HS256 ile imzalama yapılıyor ve `SigningMethodHMAC` kontrolü var → `alg: none` benzeri saldırılara karşı temel koruma mevcut.
|
||||||
|
- Access / refresh token ayrımı `TokenType` alanı ile net; `RequireAuth` yalnızca access token kabul ediyor.
|
||||||
|
- Email doğrulaması yapılmadan login’e izin verilmiyor.
|
||||||
|
|
||||||
|
- **Rol ve Yetkilendirme**
|
||||||
|
- Public API tarafında admin işlemleri `RequireAuth` + `RequireAdmin` middleware’leri ile korunuyor.
|
||||||
|
- Admin panel altındaki `"/admin"` rotaları global olarak `RequireAuth` + `RequireAdmin` ile kapalı.
|
||||||
|
|
||||||
|
- **CORS**
|
||||||
|
- DB + Redis destekli whitelist/blacklist ile **default deny** yaklaşımı kullanılıyor.
|
||||||
|
- Same-origin istekler her zaman izinli, wildcard `*` yok → klasik açık CORS yanlış konfigürasyonları görülmedi.
|
||||||
|
|
||||||
|
- **Rate Limiting**
|
||||||
|
- `/api/v1` için global; `/auth/login` ve `/auth/refresh` için isimlendirilmiş rate limit profilleri tanımlı.
|
||||||
|
- Redis tabanlı sayaçlar ile limit aşıldığında `429` ve `Retry-After` header’ı dönüyor.
|
||||||
|
|
||||||
|
- **Admin Oturumu (Browser)**
|
||||||
|
- `admin_session` cookie: `HttpOnly`, `Secure`, `SameSite=Strict` → XSS sonrası cookie çalınması ve CSRF riskleri azaltılmış.
|
||||||
|
- Admin login’de parolalar `bcrypt` ile doğrulanıyor.
|
||||||
|
|
||||||
|
### 3. Tespit Edilen Riskler ve Öneriler
|
||||||
|
|
||||||
|
#### 3.1 Refresh Token Rotation & Revoke Eksikliği (Kritik / Yüksek Öncelik)
|
||||||
|
|
||||||
|
- **Durum**:
|
||||||
|
- `/api/v1/auth/refresh` endpoint’i yalnızca:
|
||||||
|
- JWT imzasını,
|
||||||
|
- `TokenType == refresh` olmasını
|
||||||
|
kontrol ediyor.
|
||||||
|
- Refresh token’lar için DB/Redis tabanlı bir “token store”, revoke listesi veya rotation takibi yok.
|
||||||
|
- **Risk**:
|
||||||
|
- Bir refresh token ele geçirilirse, süresi dolana kadar sınırsız sayıda yeni access token üretmek için yeniden kullanılabilir.
|
||||||
|
- Token reuse (aynı refresh token’ın birden fazla kez kullanılması) tespit edilemiyor.
|
||||||
|
- **Öneri**:
|
||||||
|
- Refresh token’lar için tablo veya Redis store tasarla:
|
||||||
|
- Her refresh isteğinde:
|
||||||
|
- Eski refresh token’ı **geçersiz** kıl (rotation),
|
||||||
|
- Yeni bir refresh token üret ve store’a kaydet.
|
||||||
|
- Aynı refresh token ikinci kez kullanılırsa:
|
||||||
|
- İlgili hesabı veya oturumu geçici olarak kilitle,
|
||||||
|
- Gerekirse tüm tokenlarını revoke et (global logout).
|
||||||
|
- Mümkünse refresh token’ları **HTTP-only cookie** ile taşı (XSS’e karşı daha dirençli).
|
||||||
|
|
||||||
|
#### 3.2 API Logout / Token İptali Eksikliği (Orta–Yüksek)
|
||||||
|
|
||||||
|
- **Durum**:
|
||||||
|
- Public API’de `/api/v1/auth/logout` benzeri bir endpoint yok.
|
||||||
|
- Client tarafında yalnızca local storage / memory’den token silinerek logout yapılıyor; backend tarafında “session state” yok.
|
||||||
|
- **Risk**:
|
||||||
|
- Bir access veya refresh token sızdığında, expire olana kadar backend tarafında bunu geçersiz kılma imkânı yok.
|
||||||
|
- Özellikle refresh token için kritik: saldırgan elinde refresh token olduğu sürece yeni access token üretebilir.
|
||||||
|
- **Öneri**:
|
||||||
|
- `/api/v1/auth/logout` endpoint’i ekle:
|
||||||
|
- İlgili kullanıcının aktif refresh token kaydını (veya kayıtlardan birini) revoke listesine ekle ya da store’dan sil.
|
||||||
|
- İsteğe bağlı olarak access token için kısa süreli bir blacklist kullan (jti/subject bazlı).
|
||||||
|
- Admin panel logout şu an cookie temizliyor; bunu backend tarafında da bir “session invalidation” akışı ile desteklemek düşünülebilir.
|
||||||
|
|
||||||
|
#### 3.3 Token İçeriğinin Loglanması (Düşük–Orta)
|
||||||
|
|
||||||
|
- **Durum**:
|
||||||
|
- `GenerateTokenPair` içinde development ortamında hem access hem refresh token string’leri loglanıyor.
|
||||||
|
- Refresh akışında `fmt.Println(accessToken, "Access Token Yenilendi !!!")` ile access token stdout’a yazılıyor.
|
||||||
|
- **Risk**:
|
||||||
|
- Production konfigürasyonu yanlış yapılırsa, log dosyalarında tam token değerleri yer alabilir.
|
||||||
|
- **Öneri**:
|
||||||
|
- Production ortamında:
|
||||||
|
- Token gövdesini **asla** loglama; yalnızca `userID`, `exp`, `tokenType` gibi meta verileri logla.
|
||||||
|
- Development ortamında bile mümkünse:
|
||||||
|
- Token’ı maskeleyerek veya kısmi göstererek logla (örneğin sadece ilk 6 + son 4 karakter).
|
||||||
|
|
||||||
|
#### 3.4 Admin Login – Captcha / Turnstile Doğrulaması Tamamlanmamış (Orta)
|
||||||
|
|
||||||
|
- **Durum**:
|
||||||
|
- Admin login formu `cf-turnstile-response` alanını okuyor ancak gerçek Cloudflare Turnstile doğrulaması yapılmıyor.
|
||||||
|
- Rate limiting mevcut olsa da insan/makine ayrımı yok.
|
||||||
|
- **Risk**:
|
||||||
|
- Admin hesabı için brute force ve credential stuffing saldırılarına karşı savunma zayıf kalıyor.
|
||||||
|
- **Öneri**:
|
||||||
|
- Cloudflare Turnstile veya benzeri servis için gerçek HTTP doğrulamasını ekle:
|
||||||
|
- Turnstile token’ı backend’de doğrulanmadan login’e izin verme.
|
||||||
|
- Başarısız giriş denemelerine göre:
|
||||||
|
- IP ve hesap bazlı ek limitler veya geçici hesap kilitleme mekanizması eklemeyi değerlendir.
|
||||||
|
|
||||||
|
#### 3.5 Redis Yoksa Rate Limit & CORS Enforcement’ın Devre Dışı Kalması (Düşük–Orta)
|
||||||
|
|
||||||
|
- **Durum**:
|
||||||
|
- Redis bağlantısı yoksa, rate limit ve CORS cache tarafı graceful fail yapıyor ve bazı kontroller uygulanmayabiliyor.
|
||||||
|
- **Risk**:
|
||||||
|
- Production’da Redis yanlış konfigüre edilirse, rate limit fiilen devre dışı kalabilir; CORS kontrolleri de zayıflayabilir.
|
||||||
|
- **Öneri**:
|
||||||
|
- Production ortamında Redis’i **zorunlu bağımlılık** haline getir:
|
||||||
|
- Redis’e bağlanılamıyorsa servisi başlatma (fail-fast).
|
||||||
|
- Redis bağlantı hatalarını loglarda daha yüksek seviye (error) olarak işaretle.
|
||||||
|
|
||||||
|
### 4. `go vet` Çıktısı Özeti
|
||||||
|
|
||||||
|
- `go vet ./...` komutu çalıştırıldığında:
|
||||||
|
- `scripts/seed.go` içinde aynı pakette birden fazla `main` fonksiyonu olduğu için “main redeclared” uyarısı veriliyor.
|
||||||
|
- **Not**:
|
||||||
|
- Bu, güvenlikten ziyade script yapısına dair yapısal bir uyarı; istenirse ilgili script ayrı bir pakete veya dosya yapısına taşınarak temizlenebilir.
|
||||||
|
|
||||||
|
### 5. Önerilen İyileştirme Planı (Önceliklendirilmiş)
|
||||||
|
|
||||||
|
1. **Kritik (kısa vadede)**:
|
||||||
|
- Refresh token için rotation + revoke mekanizması tasarlayıp uygulamak.
|
||||||
|
- Public API için `/api/v1/auth/logout` endpoint’i ekleyip refresh (ve gerekiyorsa access) token’larını server-side olarak da geçersiz kılmak.
|
||||||
|
- Production’da token içeriğini loglamayı tamamen kapatmak; development’ta da maskelemek.
|
||||||
|
|
||||||
|
2. **Orta vadede**:
|
||||||
|
- Admin login için gerçek Turnstile (veya eşdeğer captcha) doğrulamasını devreye almak.
|
||||||
|
- Redis’i production ortamında zorunlu hale getirip rate limit/CORS’un Redis olmadan çalışmamasını sağlamak (fail-fast yaklaşımı).
|
||||||
|
|
||||||
|
3. **Uzun vadede**:
|
||||||
|
- Bu rapora göre hazırlanmış, dış pentest’e verilebilecek detaylı bir test senaryoları dokümanı oluşturmak (mevcut `guvenlik.md` şablonunu projeye özgü endpoint/roller ile doldurmak).
|
||||||
|
|
||||||
77
guvenlik.md
Normal file
77
guvenlik.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Go Backend API Güvenlik Tarama / Pentest Prompt (Türkçe)
|
||||||
|
|
||||||
|
Aşağıdaki prompt'u otomatik bir API güvenlik aracına, dışarıdan test yapacak bir pentester'a veya güvenlik-odaklı bir LLM'ye ver. Testlerin üretim ortamında yapılmaması, veya yapılacaksa kesin izin ve rollback planı gerektirdiğini unutma — yasal izinleri sağla.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------
|
||||||
|
Amaç
|
||||||
|
- Go tabanlı API uygulamasının güvenliğini değerlendirmek: yetkisiz erişim, yetki yükseltme, token yönetimi hataları, kimlik doğrulama/oturum güvenliği, API mantık hataları, veri sızıntıları ve yaygın web/API zafiyetleri.
|
||||||
|
- Özellikle admin endpoint’lerinin korunması, JWT (access) ve refresh-token mekanizmasının güvenli biçimde uygulanıp uygulanmadığına odaklan.
|
||||||
|
|
||||||
|
Kapsam
|
||||||
|
- Base URL: <API_BASE_URL> (ör. https://api.example.com)
|
||||||
|
- Dahil olan yollar (örnek, tam listeyi test öncesi sağla):
|
||||||
|
- /api/v1/auth/login
|
||||||
|
- /api/v1/auth/refresh
|
||||||
|
- /api/v1/users/*
|
||||||
|
- /api/v1/admin/*
|
||||||
|
- /api/v1/public/*
|
||||||
|
- Test ortamı mı yoksa üretim mi: <TEST_ENVIRONMENT_INFO> (SAĞLAYIN)
|
||||||
|
- Teste dahil edilmemesi gereken kaynaklar / IP’ler: <EXCLUDE_IF_ANY>
|
||||||
|
|
||||||
|
Yetkilendirme & Test Hesapları (TEST VERİSİ sağlanmalı)
|
||||||
|
- Test kullanıcısı (standard user): kullanıcı adı/email ve parola
|
||||||
|
- Test admin hesabı: kullanıcı adı/email ve parola (manuel veya token)
|
||||||
|
- Örnek erişim tokenı (opsiyonel): Bearer <JWT_ACCESS_TOKEN_EXAMPLE>
|
||||||
|
- Refresh token davranışı: cookie mi, bearer mi, HTTP-only mi, rotating mı? (AÇIKLA)
|
||||||
|
- JWT imzalama algoritması: (HS256/RS256/ES256/…) ve varsa public key / JWKS endpointi
|
||||||
|
- Rate limit bilgisi ve beklenen limitler
|
||||||
|
|
||||||
|
İzinler ve Güvenlik Kuralları
|
||||||
|
- Yasal izin: Test için gerekli izinler verildi mi? (EVET/HAYIR)
|
||||||
|
- Üretime doğrudan zarar verici testler yalnızca açık izin varsa yapılır. (Destruktif işlemler = veri silme, maskeleme, yeniden başlatma gibi)
|
||||||
|
- Testler sırasında tetiklenen kritik durumlarda iletişim kişisi: <AD - TEL/EMAIL>
|
||||||
|
|
||||||
|
Test Senaryoları / Yapılacaklar (ayrıntılı)
|
||||||
|
1) Kimlik Doğrulama & Token Yönetimi
|
||||||
|
- Doğrulama akışını tam tekrar et: login -> access token al -> protected endpoint çağır -> refresh token ile yeni access al.
|
||||||
|
- Access tokenın süresi dolduğunda refresh akışını test et.
|
||||||
|
- Refresh token tekrar kullanılabilir mi? (refresh token reuse / replay attack testi)
|
||||||
|
- Refresh token rotation: her refresh işleminde eski refresh token invalid ediliyor mu?
|
||||||
|
- Logout davranışı: logout sonrasında access/refresh tokenların geçersizleştirildiğini doğrula.
|
||||||
|
- Token saklama: refresh token cookie ise Secure, HttpOnly, SameSite ayarlarını kontrol et.
|
||||||
|
|
||||||
|
2) JWT Güvenlik Testleri
|
||||||
|
- Alg none veya algoritma manipülasyon testleri (alg değiştirme, key confusion).
|
||||||
|
- HS256/RS256 anahtar/alg uyumsuzluk zafiyetleri kontrolü.
|
||||||
|
- JWT imzalanmamış veya hatalı imzalı token ile erişim denemeleri.
|
||||||
|
- Exp (expiration), nbf, iat gibi claim kontrolleri; ileri/geri tarihli token denemeleri.
|
||||||
|
- Uzun süreli tokenlarda (refresh) reuse/replay tespitleri.
|
||||||
|
- JWT içindeki role/claims manipülasyonu ile RBAC atlanabilir mi? (ör. role: user -> role: admin)
|
||||||
|
|
||||||
|
3) Yetki Kontrolleri & IDOR / Horizontal/Vertical Privilege Escalation
|
||||||
|
- Admin endpointlerine kullanıcı tokenı ile erişim denemeleri.
|
||||||
|
- IDOR kontrolleri: kullanıcının başka bir kullanıcının verisini görmesi/işlemesi (ör. /users/{id} parametre manipulasyonu).
|
||||||
|
- Resource-level access kontrolü (özellikle PUT/DELETE işlemleri).
|
||||||
|
|
||||||
|
4) Endpoint Mantık / İş Akışı Testleri
|
||||||
|
- İş mantığı kaynaklı hatalar: ödeme, bakiye, onay vb. süreçlerde yetki veya mantık atlatılabiliyor mu?
|
||||||
|
- Sequence / race condition testleri (ör. aynı anda iki refresh isteği).
|
||||||
|
|
||||||
|
5) Girdi Doğrulama & Enjeksiyonlar
|
||||||
|
- SQL Injection (parametrized query kontrolü), NoSQL injection (eğer NoSQL kullanılıyorsa), LDAP injection.
|
||||||
|
- Command injection, OS command dahil kod enjeksiyonu (varsa endpointlerle).
|
||||||
|
- XSS (reflected/stored) – API cevaplarında HTML içerik döndürülüyorsa.
|
||||||
|
- XML External Entity (XXE) (eğer XML işleniyorsa).
|
||||||
|
- JSON payload fuzzing ve boundary testleri (çok büyük/değişken değerler).
|
||||||
|
|
||||||
|
6) Rate Limiting & Brute Force
|
||||||
|
- Login endpointine brute-force testleri (kaba kuvvet). Var ise account lockout, rate limiting ve CAPTCHA mekanizmalarını kontrol et.
|
||||||
|
- Token brute-force / token guessing zafiyetleri (zayıf token üretimi).
|
||||||
|
|
||||||
|
7) Transport Güvenliği & CORS
|
||||||
|
- TLS konfigürasyonu: güncel protokoller (TLS1.2/1.3), zayıf şifreler, HSTS header kontrolü.
|
||||||
|
- CORS konfigürasyonu: permissive wildcard origin veya credentials ile açık origin olup olmadığı.
|
||||||
|
- HSTS, CSP, X-Frame-Options, X-Content-Type-Options gibi güvenlik header’ları.
|
||||||
|
|
||||||
|
8) Bilgi Sızıntısı & Logging
|
||||||
|
- H
|
||||||
6
info.log
Normal file
6
info.log
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
2026-04-08T15:00:03.318+0300 INFO config/loger.go:38 Logger başlatıldı (konsol + info.log)
|
||||||
|
2026-04-08T15:00:03.319+0300 INFO config/mysql_db.go:24 Yapılandırmada DB_URL bulundu, veritabanına bağlanılmaya çalışılıyor...
|
||||||
|
2026-04-08T15:00:03.323+0300 INFO config/mysql_db.go:43 MySQL veritabanı bağlantısı kuruldu.
|
||||||
|
2026-04-08T15:00:03.326+0300 INFO config/redis_db.go:47 Connected to Redis successfully
|
||||||
|
2026-04-08T15:00:03.660+0300 INFO migrate/migrate.go:41 AutoMigrate Yapıldı.
|
||||||
|
2026-04-08T15:00:03.662+0300 INFO aresv2/main.go:34 Init Uygulandı !!
|
||||||
131
main.go
Normal file
131
main.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
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/database/seeder"
|
||||||
|
_ "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: {}
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user