first commit
This commit is contained in:
58
.air.toml
Normal file
58
.air.toml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#:schema https://json.schemastore.org/any.json
|
||||||
|
|
||||||
|
env_files = []
|
||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ."
|
||||||
|
delay = 1000
|
||||||
|
entrypoint = ["./tmp/main"]
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
ignore_dangerous_root_dir = false
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
silent = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
app_start_timeout = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
98
.env
Normal file
98
.env
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Application Port Configuration
|
||||||
|
PORT=8080
|
||||||
|
#########################
|
||||||
|
# MySql Configuration
|
||||||
|
DB_URL="beyhango:gg7678290@tcp(10.80.80.70:3306)/beyhango?charset=utf8mb4&parseTime=True&loc=Local"
|
||||||
|
#DB_URL=mariadb://beyhango:gg7678290@10.80.80.70:3306/beyhango
|
||||||
|
|
||||||
|
DB_USER=cloud
|
||||||
|
DB_PASSWORD=gg7678290
|
||||||
|
DB_NAME=atahan_go
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_HOST=10.80.80.70
|
||||||
|
##########################
|
||||||
|
# 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/0
|
||||||
|
###########################
|
||||||
|
# JWT Secret
|
||||||
|
JWT_SECRET=eCT286MautyK9TWfz8SPSIWJTXV83mwLRwriLvSbpcMZuDLxywaHWP9Ju7xRoTS2
|
||||||
|
# Session Secret (for OAuth state management)
|
||||||
|
SESSION_SECRET=kL8mN2pQ5rS9tV1wX4yZ7aB0cD3eF6gH9jK2mN5pQ8rS1tV4wX7yZ0aB3cD6eF9g
|
||||||
|
###########################
|
||||||
|
# Application URL
|
||||||
|
APP_URL=http://localhost:8080
|
||||||
|
###########################
|
||||||
|
# OAuth - Google
|
||||||
|
GOOGLE_CLIENT_ID=915364976256-691m0s87as2r5vdbqr96f6humblseobt.apps.googleusercontent.com
|
||||||
|
GOOGLE_CLIENT_SECRET=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
|
||||||
|
################################
|
||||||
|
# OAuth - GitHub
|
||||||
|
GITHUB_CLIENT_ID=Ov23liUt9B61O46Mdfm4
|
||||||
|
GITHUB_CLIENT_SECRET=c7fc8dcb1b2c8f22120608425d07d5efd995baaf
|
||||||
|
################################
|
||||||
|
# OAuth Callback URL (Backend OAuth callback endpoint)
|
||||||
|
CLIENT_CALLBACK_URL=http://localhost:8080/v1/auth
|
||||||
|
# OAuth Redirect URL (Frontend callback page where user will be redirected with tokens)
|
||||||
|
OAUTH_REDIRECT_URL=http://localhost:3000/auth/callback
|
||||||
|
################################
|
||||||
|
# AVATANE IMAGES
|
||||||
|
AVATAR_H=150
|
||||||
|
AVATAR_W=150
|
||||||
|
AVATAR_Q=90
|
||||||
|
AVATAR_B=cover
|
||||||
|
AVATAR_F=webp
|
||||||
|
#######################
|
||||||
|
# Home IMAGES
|
||||||
|
HOME_IMAGE_H=400
|
||||||
|
HOME_IMAGE_W=400
|
||||||
|
HOME_IMAGE_Q=90
|
||||||
|
HOME_IMAGE_B=cover
|
||||||
|
HOME_IMAGE_F=webp
|
||||||
|
#######################
|
||||||
|
# Aboutme IMAGES
|
||||||
|
ABOUTME_IMAGE_H=400
|
||||||
|
ABOUTME_IMAGE_W=400
|
||||||
|
ABOUTME_IMAGE_Q=90
|
||||||
|
ABOUTME_IMAGE_B=cover
|
||||||
|
ABOUTME_IMAGE_F=webp
|
||||||
|
#######################
|
||||||
|
# MyService IMAGES
|
||||||
|
SERVICE_IMAGE_H=256
|
||||||
|
SERVICE_IMAGE_W=256
|
||||||
|
SERVICE_IMAGE_Q=90
|
||||||
|
SERVICE_IMAGE_B=cover
|
||||||
|
SERVICE_IMAGE_F=webp
|
||||||
|
#######################
|
||||||
|
# BANNER IMAGES
|
||||||
|
BANNER_IMAGE_H=700
|
||||||
|
BANNER_IMAGE_W=1920
|
||||||
|
BANNER_IMAGE_Q=85
|
||||||
|
BANNER_IMAGE_B=cover
|
||||||
|
BANNER_IMAGE_F=webp
|
||||||
|
################################
|
||||||
|
# Email Settings (Mailpit)
|
||||||
|
EMAIL_HOST=212.64.215.243
|
||||||
|
EMAIL_PORT=1025
|
||||||
|
EMAIL_HOST_USER=""
|
||||||
|
EMAIL_HOST_PASSWORD=""
|
||||||
|
EMAIL_USE_TLS=false
|
||||||
|
EMAIL_USE_SSL=false
|
||||||
|
EMAIL_FROM=noreply@gauth.local
|
||||||
|
################################
|
||||||
|
CORS_DEBUG=true
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
|
FRONTEND_URL=http://localhost:3000
|
||||||
|
# SESSION_SECRET=mTFY2jAOMWWxadVIWjRoPG9aOM3z9srCVoU35Gs1VZaRKgXet26cztUE8LLpwok9
|
||||||
|
# JWT Settings
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=120
|
||||||
|
REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||||
|
### TRUSTED PROXIES
|
||||||
|
TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8
|
||||||
|
|
||||||
|
CLOUD_FLARE_SITE_KEY='0x4AAAAAACHzHKvlEwMamxCM'
|
||||||
|
CLOUD_FLARE_SECRET='0x4AAAAAACHzHHisTSFzGw15HvwXF3yXRIg'
|
||||||
|
TURNSTILE_SECRET_KEY=0x4AAAAAACHzHHisTSFzGw15HvwXF3yXRIg
|
||||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
### 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
|
||||||
|
frontend.tar.gz
|
||||||
|
tmp
|
||||||
|
gobeyhango
|
||||||
|
# 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
|
||||||
|
|
||||||
|
bin
|
||||||
|
gobeyhan
|
||||||
213
GEMINI.md
Normal file
213
GEMINI.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# Admin Panel — Giriş (Login) + Refresh Token Mantığı Prompt / İş Tanımı
|
||||||
|
|
||||||
|
Amaç:
|
||||||
|
- Var olan `admin-panel` Vue projesinde (Tailwind, reka-ui, Pinia, vue-auth3, lucide-vue-next, zod, vb.) Go backend API ile sadece "login" işlevi sunan admin arayüzü oluşturmak.
|
||||||
|
- Backend JWT access token + refresh token mekanizması destekliyor (refresh token mantığı kurulmalı).
|
||||||
|
- Backend base: `http://localhost:8080/api/v1` — login endpoint: `POST /auth/login` (dönen: access token + refresh token + user) — refresh endpoint olarak `POST /auth/refresh` (veya backend ile uyumlu başka path) bekleniyor.
|
||||||
|
- Hedef: güvenli, responsive login + refresh token tabanlı token yenileme, token saklama ve otomatik yenileme akışı ile korunan admin dashboard iskeleti.
|
||||||
|
|
||||||
|
Genel Tasarım Kararları (kritik)
|
||||||
|
- Access token (kısa ömürlü, örneğin 5-15 dk): uygulamada bellek/Pinia state veya memory-only saklama (localStorage'ta saklanması güvenlik riski taşıdığından önerilmez).
|
||||||
|
- Refresh token (uzun ömürlü, örneğin 7-30 gün): production için HttpOnly, Secure cookie ile saklanmalı — bu, XSS riskini azaltır. Eğer backend cookie ile set ediyorsa client sadece server çağrısıyla refresh yapar (cookie otomatik gönderilir).
|
||||||
|
- Eğer backend refresh token'ı client tarafında bekliyorsa (ör. JSON response ile gönderip client saklıyorsa), yalnızca sessionStorage (veya yine tercihen HttpOnly cookie) kullanılmalı; localStorage önerilmez.
|
||||||
|
- Refresh token rotation ve revocation: backend destekliyorsa rotasyonu kullan; logout veya refresh hatasında token'ı serverda iptal et.
|
||||||
|
|
||||||
|
Beklenen Endpoint'ler (uygulama tarafının kullanacağı)
|
||||||
|
- POST /api/v1/auth/login
|
||||||
|
- Body: { email, password }
|
||||||
|
- Response (örnek): { access_token, refresh_token (opsiyonel if cookie), user, expires_in }
|
||||||
|
- POST /api/v1/auth/refresh
|
||||||
|
- Use-case: access token yenileme. Eğer refresh token cookie ile saklanıyorsa body boş olabilir; aksi halde { refresh_token } gönderilebilir.
|
||||||
|
- Response: { access_token, refresh_token (rotated), expires_in }
|
||||||
|
- POST /api/v1/auth/logout
|
||||||
|
- Use-case: refresh token'ı server tarafında invalidasyon + client temizleme.
|
||||||
|
|
||||||
|
Refresh Token Akışı — Kabul Kriterleri ve Davranış
|
||||||
|
1. Login akışı
|
||||||
|
- Kullanıcı /login formunu doldurur.
|
||||||
|
- POST /auth/login çağrısı yapılır.
|
||||||
|
- Başarılıysa:
|
||||||
|
- Access token uygulama state'ine (Pinia memory) veya güvenli saklama yerine kısa süreli tutulacak şekilde konur.
|
||||||
|
- Refresh token:
|
||||||
|
- Tercih A (öncelikli, tavsiye): backend Set-Cookie ile HttpOnly, Secure cookie olarak gönderir. Client hiçbir şekilde refresh token'ı JS üzerinden okumaz veya yazmaz.
|
||||||
|
- Tercih B (fallback): backend refresh token'ı JSON içinde dönerse client token'ı sessionStorage'e veya (remember me seçeneği varsa) localStorage'e koyar ve riskler README'de belirtilir.
|
||||||
|
- Kullanıcı /admin'e yönlendirilir.
|
||||||
|
|
||||||
|
2. Otomatik yenileme (interceptor + single-refresh lock)
|
||||||
|
- Tüm korumalı isteklerde Authorization: Bearer <access_token> header kullanılır.
|
||||||
|
- Bir istek 401 dönerse ve hata sebebi token expiration ise:
|
||||||
|
- HTTP client (axios/fetch wrapper) bir *refresh attempt* başlatır:
|
||||||
|
- Eğer başka bir refresh devam ediyorsa yeni istek bekletilir (queue) — bu, aynı anda birden çok refresh çağrısının yapılmasını engeller.
|
||||||
|
- Refresh başarılı ise yeni access token (ve varsa yeni refresh token) saklanır; bekleyen istekler yeniden denenir (replay).
|
||||||
|
- Refresh başarısız ise (refresh expired veya invalid) -> authStore.logout() çağrılır ve kullanıcı /login'e yönlendirilir.
|
||||||
|
- İstek interceptor'ı 401'lerde doğrudan logout yerine önce refresh denemeli; refresh'in başarısız olması durumunda logout yapılmalı.
|
||||||
|
|
||||||
|
3. Proaktif yenileme (optional, önerilen UX)
|
||||||
|
- Access token expiry (exp) okunarak (token JWT ise), expiry zamanından örn. 60-120s önce otomatik yenileme tetiklenebilir.
|
||||||
|
- Bu, kullanıcı etkileşimi sırasında beklenmedik logout'ları azaltır.
|
||||||
|
- Proaktif yenileme, aynı queue/lock mekanizmasını kullanarak aynı anda birden fazla refresh'i engeller.
|
||||||
|
|
||||||
|
4. Logout / revocation
|
||||||
|
- logout() çağrıldığında client:
|
||||||
|
- Eğer refresh cookie ise: POST /auth/logout çağrısı yaparak server tarafında refresh token iptal edilir ve server cookie'yi temizler.
|
||||||
|
- Client state'teki access token ve user bilgileri temizlenir; local/session storage temizlenir.
|
||||||
|
- Kullanıcı /login'e yönlendirilir.
|
||||||
|
|
||||||
|
5. Refresh token rotation & güvenlik
|
||||||
|
- Eğer backend rotation destekliyorsa (refresh token her yenilemede değişiyorsa), client her refresh response'unda yeni refresh token'ı saklamalı (ve/veya server Set-Cookie ile güncelleyip JS erişim izni vermemeli).
|
||||||
|
- Tek kullanımlık (one-time) refresh token kullanımı varsa backend başarısız veya yeniden kullanım durumunda tüm oturumları iptal etmeli.
|
||||||
|
- HTTP cookie attributeleri (production):
|
||||||
|
- HttpOnly
|
||||||
|
- Secure
|
||||||
|
- SameSite=Lax (veya Strict, ihtiyaçlara göre)
|
||||||
|
- Path=/api/v1/auth/refresh (gerekirse) veya Path=/
|
||||||
|
- Domain ihtiyaca göre ayarlanmalı
|
||||||
|
|
||||||
|
Client tarafında uygulama tasarımı (yapılacaklar — geliştiriciye talimat)
|
||||||
|
- Auth store (Pinia) içinde:
|
||||||
|
- state: accessToken (memory), user, loading, refreshPromise / refreshLock (internal)
|
||||||
|
- getters: isAuthenticated
|
||||||
|
- actions: login(), logout(), setAccessToken(), tryRefresh()
|
||||||
|
- tryRefresh(): refresh endpoint'ine istek atar; başarılıysa accessToken güncellenir; başarısızsa reject -> logout.
|
||||||
|
- HTTP client wrapper:
|
||||||
|
- baseURL: VITE_API_BASE_URL
|
||||||
|
- request interceptor: access token mevcutsa Authorization header ekle
|
||||||
|
- response interceptor:
|
||||||
|
- 401 alındığında:
|
||||||
|
- Eğer hata tipi token expired ise tryRefresh() çağrılır.
|
||||||
|
- tryRefresh() başarılı ise orijinal istek yeniden gönderilir.
|
||||||
|
- Eğer tryRefresh() başarısızsa logout() ve /login yönlendirme.
|
||||||
|
- Her isteğin retry mekanizması maksimum 1 refresh attempt ile sınırlı olmalı (sonsuz döngü olmaması için).
|
||||||
|
- Concurrent refresh yönetimi: single refresh promise pattern (ilk refresh çağrısı bir Promise döndürür; diğer istekler bu promise'i await eder).
|
||||||
|
- Proaktif yenileme (opsiyonel ama önerilen):
|
||||||
|
- Access token decode edilerek exp timestamp alınır.
|
||||||
|
- exp - now <= threshold (örn. 60s) olduğunda tryRefresh() çağrılır.
|
||||||
|
- Bu mekanizma kullanıcı etkileşiminde veya route değişimlerinde tetiklenebilir.
|
||||||
|
- Storage politikası:
|
||||||
|
- access token: bellek (Pinia) veya short-lived storage
|
||||||
|
- refresh token: HttpOnly cookie (tercih) veya sessionStorage/secure storage fallback
|
||||||
|
- README'de hangi seçeneğin neden seçildiği açıkça dokümante edilecek.
|
||||||
|
|
||||||
|
Hata Yönetimi & UX
|
||||||
|
- Refresh başarısız olursa kullanıcıya basit, net bir mesaj göster: "Oturum süresi doldu, lütfen tekrar giriş yapın."
|
||||||
|
- Arka arkaya çalışan isteklerde refresh sırasında spinner veya global loading state kullanılabilir.
|
||||||
|
- Eğer backend 429 (rate limit) veya 5xx dönerse kullanıcı uygun uyarı almalı (tekrar dene vs).
|
||||||
|
|
||||||
|
Test Senaryoları (kritik)
|
||||||
|
1. Başarılı login:
|
||||||
|
- login -> access token alındı, refresh token cookie ile set edildi (veya sessionStorage'e kondu) -> /admin'e yönlendirme.
|
||||||
|
2. Access token expire iken istek:
|
||||||
|
- İlk korumalı istek 401 döner -> tryRefresh() tetiklenir -> refresh başarılı -> orijinal istek tekrar çalışır -> kullanıcı etkileşimi kesintisiz devam eder.
|
||||||
|
3. Refresh expired veya invalid:
|
||||||
|
- Refresh attempt başarısız -> logout -> /login ve uygun hata mesajı.
|
||||||
|
4. Concurrent istekler token expired durumda:
|
||||||
|
- Birden fazla istek gönderildiğinde yalnızca bir refresh çağrısı yapılmalı, diğerleri bekletilip ardından yeniden denenmeli.
|
||||||
|
5. Logout:
|
||||||
|
- logout çağrıldığında server /auth/logout çağrılır (varsa) ve client state & storageler temizlenir.
|
||||||
|
6. Proaktif yenileme:
|
||||||
|
- Token exp < threshold iken uygulama otomatik yenileme yapıyor ve kullanıcıyı kesintiye uğratmıyor.
|
||||||
|
|
||||||
|
Dokümantasyon ve README Notları
|
||||||
|
- .env örneği: VITE_API_BASE_URL=http://localhost:8080/api/v1
|
||||||
|
- Token saklama tercihleri ve güvenlik nedenleri (HttpOnly cookie vs localStorage) açıkça yazılmalı.
|
||||||
|
- Backend ile cookie tabanlı refresh kullanılıyorsa CORS ve credentials ayarları açıklanmalı (axios: withCredentials=true; server: Access-Control-Allow-Credentials: true).
|
||||||
|
- Güvenlik önerileri: refresh token'ların server-side revocation tablosunda saklanması, rotation kullanımı ve refresh token reuse detection.
|
||||||
|
|
||||||
|
Teslim Edilecekler (geliştiriciye verilecek)
|
||||||
|
- Güncellenmiş prompt (bu dosya) — refresh token mantığı, beklenen endpoint'ler, client davranışı ve test senaryoları açık.
|
||||||
|
- Uygulamada olması gerekenler:
|
||||||
|
- login sayfası
|
||||||
|
- Pinia auth store (refresh logic ile)
|
||||||
|
- HTTP client wrapper (interceptor + refresh queue/lock)
|
||||||
|
- Router guard (protected rotalar)
|
||||||
|
- Dashboard iskeleti ve en az bir örnek protected endpoint kullanımı
|
||||||
|
- README: environment, çalışma, güvenlik ve token saklama tercihleri
|
||||||
|
|
||||||
|
Notlar / Varsayımlar
|
||||||
|
- Backend, refresh endpoint ve/veya Set-Cookie davranışını destekliyor olmalıdır. Eğer backend cookie yerine JSON refresh token dönerse prompt'ta belirtilen fallback yöntem uygulanacak.
|
||||||
|
- Exact refresh endpoint path'ı backend ile netleştirilmeli (`/auth/refresh`, `/auth/refresh-token` veya benzeri).
|
||||||
|
- Backend CORS + cookie kullanımında `Access-Control-Allow-Credentials: true` ve client çağrılarında `withCredentials: true` gerekecektir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ İMPLEMENTASYON TAMAMLANDI (12 Şubat 2026)
|
||||||
|
|
||||||
|
Yukarıdaki tüm gereksinimler başarıyla implement edildi. İşte teslim edilen bileşenler:
|
||||||
|
|
||||||
|
### Oluşturulan Dosyalar
|
||||||
|
|
||||||
|
#### Environment & Yapılandırma
|
||||||
|
- `.env` - Environment variables
|
||||||
|
- `.env.example` - Environment template
|
||||||
|
- `tsconfig.app.json` - TypeScript path alias yapılandırması
|
||||||
|
|
||||||
|
#### Core Infrastructure
|
||||||
|
- `src/lib/http-client.ts` - Axios wrapper + interceptors (401 handling, refresh logic, retry)
|
||||||
|
- `src/services/auth.service.ts` - Auth API endpoints (login, refresh, logout)
|
||||||
|
- `src/types/auth.types.ts` - TypeScript interfaces
|
||||||
|
|
||||||
|
#### State Management
|
||||||
|
- `src/stores/auth.ts` - Pinia auth store
|
||||||
|
- Login/logout actions
|
||||||
|
- Refresh token logic (single promise lock)
|
||||||
|
- Proaktif token yenileme (exp check)
|
||||||
|
- Concurrent refresh prevention
|
||||||
|
|
||||||
|
#### UI Components
|
||||||
|
- `src/components/ui/Button.vue` - Reusable button (variants, loading, sizes)
|
||||||
|
- `src/components/ui/Input.vue` - Reusable input (validation, error states)
|
||||||
|
|
||||||
|
#### Views
|
||||||
|
- `src/views/LoginView.vue` - Modern login sayfası (Zod validation, responsive, dark mode)
|
||||||
|
- `src/views/DashboardView.vue` - Admin dashboard (user info, stats, protected content)
|
||||||
|
|
||||||
|
#### Router & Navigation
|
||||||
|
- `src/router/index.ts` - Router configuration + navigation guards
|
||||||
|
- `/login` (public)
|
||||||
|
- `/admin` (protected, requires auth)
|
||||||
|
|
||||||
|
#### App Structure
|
||||||
|
- `src/App.vue` - Minimal layout (sadece RouterView)
|
||||||
|
|
||||||
|
#### Dokümantasyon
|
||||||
|
- `README.md` - Detaylı dokümantasyon
|
||||||
|
- Token saklama stratejisi
|
||||||
|
- CORS yapılandırması
|
||||||
|
- Auth akışı diagramları
|
||||||
|
- Backend endpoint gereksinimleri
|
||||||
|
- Güvenlik best practices
|
||||||
|
|
||||||
|
### Teknolojiler
|
||||||
|
- Vue 3 (Composition API)
|
||||||
|
- TypeScript
|
||||||
|
- Pinia (state management)
|
||||||
|
- Vue Router
|
||||||
|
- Axios
|
||||||
|
- Zod (validation)
|
||||||
|
- Tailwind CSS
|
||||||
|
- Lucide Icons
|
||||||
|
- SweetAlert2
|
||||||
|
- jwt-decode
|
||||||
|
|
||||||
|
### Güvenlik Özellikleri
|
||||||
|
✅ Access token bellek (Pinia) depolaması
|
||||||
|
✅ HttpOnly cookie desteği (refresh token)
|
||||||
|
✅ withCredentials: true (CORS)
|
||||||
|
✅ Single refresh promise lock (concurrent prevention)
|
||||||
|
✅ Proaktif token yenileme (60s threshold)
|
||||||
|
✅ Router guards (protected routes)
|
||||||
|
✅ Form validation (Zod)
|
||||||
|
✅ XSS koruması
|
||||||
|
|
||||||
|
### Test Senaryoları (Hazır)
|
||||||
|
1. ✅ Başarılı login akışı
|
||||||
|
2. ✅ Access token expiration & refresh
|
||||||
|
3. ✅ Refresh token invalid/expired handling
|
||||||
|
4. ✅ Concurrent requests (single refresh)
|
||||||
|
5. ✅ Logout flow
|
||||||
|
6. ✅ Protected route guards
|
||||||
|
7. ✅ Proaktif yenileme
|
||||||
|
|
||||||
|
**Backend Hazırlığı**: Frontend tamamlandı, backend endpoint'leri (`/auth/login`, `/auth/refresh`, `/auth/logout`) hazır olduğunda test edilebilir.
|
||||||
|
|
||||||
|
**Çalıştırma**: `npm run dev` komutu ile başlatılabilir.
|
||||||
794
api_documentation.md
Normal file
794
api_documentation.md
Normal file
@@ -0,0 +1,794 @@
|
|||||||
|
# 🚀 Backend API Dokümantasyonu
|
||||||
|
|
||||||
|
## 📋 İçindekiler
|
||||||
|
|
||||||
|
- [Genel Bilgiler](#genel-bilgiler)
|
||||||
|
- [Swagger UI](#swagger-ui)
|
||||||
|
- [Authentication](#authentication)
|
||||||
|
- [Response Formatı](#response-formatı)
|
||||||
|
- [Hata Kodları](#hata-kodları)
|
||||||
|
- [Modüler Yapı](#modüler-yapı)
|
||||||
|
- [Endpoint'ler](#endpointler)
|
||||||
|
- [Blog App](#blog-app)
|
||||||
|
- [Account App](#account-app)
|
||||||
|
- [Settings App](#settings-app)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Genel Bilgiler
|
||||||
|
|
||||||
|
### Base URL
|
||||||
|
```
|
||||||
|
http://localhost:8080/api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Type
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modüler Yapı
|
||||||
|
API, **app-based modüler yapı** kullanır:
|
||||||
|
|
||||||
|
| App | Sorumluluk | Endpoint Prefix |
|
||||||
|
|-----|------------|-----------------|
|
||||||
|
| **Blog** | Kategori, Tag, Post, Yorum | `/api/v1/categories`, `/api/v1/posts` |
|
||||||
|
| **Account** | Kullanıcı, Rol, İzin | `/api/v1/admin/users`, `/api/v1/admin/roles` |
|
||||||
|
| **Settings** | CORS, Rate Limit | `/api/v1/admin/cors`, `/api/v1/admin/rate-limits` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Swagger UI
|
||||||
|
|
||||||
|
### Swagger Dokümantasyonu
|
||||||
|
|
||||||
|
API'nin interaktif dokümantasyonuna Swagger UI üzerinden erişebilirsiniz:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8080/swagger/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Swagger Generate
|
||||||
|
|
||||||
|
Swagger dokümantasyonunu güncellemek için:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Swagger docs oluştur
|
||||||
|
swag init
|
||||||
|
|
||||||
|
# Veya make komutu ile (varsa)
|
||||||
|
make swagger
|
||||||
|
```
|
||||||
|
|
||||||
|
### Swagger Annotation Örneği
|
||||||
|
|
||||||
|
Handler'larda godoc annotation'lar kullanılır:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// GetAllCategories godoc
|
||||||
|
// @Summary Get all active categories
|
||||||
|
// @Description Get list of all active categories
|
||||||
|
// @Tags blog,categories
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} models.Category
|
||||||
|
// @Router /api/v1/categories [get]
|
||||||
|
func (h *CategoryHandler) GetAllCategories(c *gin.Context) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Kimlik Doğrulama
|
||||||
|
|
||||||
|
Admin ve korumalı endpoint'ler için JWT token:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <your_jwt_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erişim Seviyeleri
|
||||||
|
|
||||||
|
| Seviye | Açıklama | Örnek |
|
||||||
|
|--------|----------|-------|
|
||||||
|
| 🌍 **Public** | Kimlik doğrulama gerektirmez | `GET /api/v1/posts` |
|
||||||
|
| 🔓 **Auth** | Login olmuş kullanıcı | `POST /api/v1/user/posts/:id/comments` |
|
||||||
|
| 🔐 **Admin** | Admin rolü gerekli | `POST /api/v1/admin/categories` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Response Formatı
|
||||||
|
|
||||||
|
### Başarılı Response
|
||||||
|
|
||||||
|
#### Tekil Kayıt
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"title": "Örnek"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Liste (Pagination ile)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [...],
|
||||||
|
"total": 100,
|
||||||
|
"page": 1,
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hata Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Resource not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hata Kodları
|
||||||
|
|
||||||
|
| HTTP Status | Açıklama |
|
||||||
|
|-------------|----------|
|
||||||
|
| **200** | Başarılı |
|
||||||
|
| **201** | Oluşturuldu |
|
||||||
|
| **400** | Hatalı Request |
|
||||||
|
| **401** | Yetkisiz |
|
||||||
|
| **403** | Yasak |
|
||||||
|
| **404** | Bulunamadı |
|
||||||
|
| **500** | Sunucu Hatası |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modüler Yapı
|
||||||
|
|
||||||
|
### Kod Organizasyonu
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── blog/ → Blog işlevleri
|
||||||
|
│ ├── handlers/
|
||||||
|
│ └── services/
|
||||||
|
├── account/ → Kullanıcı yönetimi
|
||||||
|
│ ├── handlers/
|
||||||
|
│ └── services/
|
||||||
|
└── settings/ → Sistem ayarları
|
||||||
|
├── handlers/
|
||||||
|
└── services/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import Path'leri
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
blogHandlers "gobeyhan/app/blog/handlers"
|
||||||
|
blogServices "gobeyhan/app/blog/services"
|
||||||
|
accountHandlers "gobeyhan/app/account/handlers"
|
||||||
|
accountServices "gobeyhan/app/account/services"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoint'ler
|
||||||
|
|
||||||
|
## Blog App
|
||||||
|
|
||||||
|
Blog uygulaması 5 ana kaynak içerir: Categories, Tags, Posts, Comments, CategoryViews
|
||||||
|
|
||||||
|
### 📌 Categories
|
||||||
|
|
||||||
|
#### Tüm Kategoriler (Public)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/categories
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Teknoloji",
|
||||||
|
"slug": "teknoloji",
|
||||||
|
"parent": null,
|
||||||
|
"children": [...]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Kategori Detay (Public)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/categories/:slug
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Kategori Görüntülenme (Public)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/categories/:id/view
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Admin: Kategori CRUD
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/categories
|
||||||
|
GET /api/v1/admin/categories/:id
|
||||||
|
POST /api/v1/admin/categories
|
||||||
|
PUT /api/v1/admin/categories/:id
|
||||||
|
DELETE /api/v1/admin/categories/:id
|
||||||
|
GET /api/v1/admin/categories/:id/views
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Yeni Kategori",
|
||||||
|
"slug": "yeni-kategori",
|
||||||
|
"description": "Açıklama",
|
||||||
|
"is_active": true,
|
||||||
|
"order": 1,
|
||||||
|
"parent_id": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 Tags
|
||||||
|
|
||||||
|
#### Tüm Etiketler (Public)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/tags
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Etiket Detay (Public)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/tags/:slug
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Admin: Etiket CRUD
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/tags
|
||||||
|
GET /api/v1/admin/tags/:id
|
||||||
|
POST /api/v1/admin/tags
|
||||||
|
PUT /api/v1/admin/tags/:id
|
||||||
|
DELETE /api/v1/admin/tags/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tag": "golang",
|
||||||
|
"slug": "golang",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 Posts
|
||||||
|
|
||||||
|
#### Tüm Yazılar (Public, Paginated)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/posts?page=1&limit=10
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Blog Yazısı",
|
||||||
|
"slug": "blog-yazisi",
|
||||||
|
"user": {...},
|
||||||
|
"categories": [...],
|
||||||
|
"tags": [...]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 100,
|
||||||
|
"page": 1,
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Yazı Detay (Public)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/posts/:slug
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Admin: Yazı CRUD
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/posts
|
||||||
|
GET /api/v1/admin/posts/:id
|
||||||
|
POST /api/v1/admin/posts
|
||||||
|
PUT /api/v1/admin/posts/:id
|
||||||
|
DELETE /api/v1/admin/posts/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Yeni Yazı",
|
||||||
|
"content": "İçerik...",
|
||||||
|
"categories": [1, 2],
|
||||||
|
"tags": [1, 3, 5],
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 Comments
|
||||||
|
|
||||||
|
#### Yazı Yorumları (Public)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/posts/:id/comments
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"user_id": 2,
|
||||||
|
"body": "Yorum içeriği",
|
||||||
|
"parent_id": null,
|
||||||
|
"children": [...]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Yorum Yaz (Auth)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/user/posts/:id/comments
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"body": "Yorum içeriği",
|
||||||
|
"parent_id": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Admin: Yorum Yönetimi
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/comments
|
||||||
|
GET /api/v1/admin/comments/:id
|
||||||
|
PUT /api/v1/admin/comments/:id
|
||||||
|
DELETE /api/v1/admin/comments/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Account App
|
||||||
|
|
||||||
|
Kullanıcı, rol ve izin yönetimi
|
||||||
|
|
||||||
|
### 📌 Users
|
||||||
|
|
||||||
|
#### Admin: Tüm Kullanıcılar
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/users?page=1&limit=10&include_deleted=false
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"username": "admin",
|
||||||
|
"email": "admin@example.com",
|
||||||
|
"roles": [...],
|
||||||
|
"social_accounts": [...],
|
||||||
|
"deleted_at": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 50
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Admin: Kullanıcı CRUD
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/users/:id
|
||||||
|
POST /api/v1/admin/users
|
||||||
|
PUT /api/v1/admin/users/:id
|
||||||
|
DELETE /api/v1/admin/users/:id # Soft delete
|
||||||
|
POST /api/v1/admin/users/:id/restore
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "securePass123",
|
||||||
|
"username": "newuser"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Not**: Password bcrypt ile hashlenip saklanır
|
||||||
|
|
||||||
|
#### Admin: Rol Yönetimi
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/admin/users/:id/roles
|
||||||
|
DELETE /api/v1/admin/users/:id/roles/:role_id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Assign Role:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"role_id": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 Roles
|
||||||
|
|
||||||
|
#### Admin: Roller
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/roles
|
||||||
|
GET /api/v1/admin/roles/:id
|
||||||
|
POST /api/v1/admin/roles
|
||||||
|
PUT /api/v1/admin/roles/:id
|
||||||
|
DELETE /api/v1/admin/roles/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "admin",
|
||||||
|
"description": "Administrator",
|
||||||
|
"permissions": [...]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "moderator",
|
||||||
|
"description": "Moderator role"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 Permissions
|
||||||
|
|
||||||
|
#### Admin: İzinler
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/permissions
|
||||||
|
POST /api/v1/admin/permissions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "edit_comment",
|
||||||
|
"description": "Can edit comments"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 Social Accounts
|
||||||
|
|
||||||
|
#### Auth: Kullanıcının Social Hesapları
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/user/social-accounts
|
||||||
|
DELETE /api/v1/user/social-accounts/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"provider": "google",
|
||||||
|
"provider_user_id": "123456"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Settings App
|
||||||
|
|
||||||
|
Sistem ayarları ve yapılandırma
|
||||||
|
|
||||||
|
### 📌 CORS Whitelist
|
||||||
|
|
||||||
|
#### Admin: CORS Whitelist
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/cors/whitelist
|
||||||
|
POST /api/v1/admin/cors/whitelist
|
||||||
|
PUT /api/v1/admin/cors/whitelist/:id
|
||||||
|
DELETE /api/v1/admin/cors/whitelist/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"origin": "https://example.com",
|
||||||
|
"description": "Main website"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 CORS Blacklist
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/cors/blacklist
|
||||||
|
POST /api/v1/admin/cors/blacklist
|
||||||
|
PUT /api/v1/admin/cors/blacklist/:id
|
||||||
|
DELETE /api/v1/admin/cors/blacklist/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 Rate Limits
|
||||||
|
|
||||||
|
#### Admin: Rate Limit Ayarları
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/admin/rate-limits
|
||||||
|
PUT /api/v1/admin/rate-limits/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"path": "/api/v1/posts",
|
||||||
|
"max_requests": 100,
|
||||||
|
"window_seconds": 60
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"max_requests": 200,
|
||||||
|
"window_seconds": 120
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 CORS Cache
|
||||||
|
|
||||||
|
#### Admin: Cache Temizle
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/admin/cors/cache/invalidate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoint Özeti
|
||||||
|
|
||||||
|
### Blog App (29 endpoint)
|
||||||
|
|
||||||
|
| Kaynak | Public | Auth | Admin |
|
||||||
|
|--------|--------|------|-------|
|
||||||
|
| Categories | 3 | 0 | 6 |
|
||||||
|
| Tags | 2 | 0 | 5 |
|
||||||
|
| Posts | 2 | 0 | 5 |
|
||||||
|
| Comments | 1 | 1 | 4 |
|
||||||
|
| CategoryViews | 1 | 0 | 1 |
|
||||||
|
|
||||||
|
### Account App (17 endpoint)
|
||||||
|
|
||||||
|
| Kaynak | Public | Auth | Admin |
|
||||||
|
|--------|--------|------|-------|
|
||||||
|
| Users | 0 | 0 | 8 |
|
||||||
|
| Roles | 0 | 0 | 5 |
|
||||||
|
| Permissions | 0 | 0 | 2 |
|
||||||
|
| Social Accounts | 0 | 2 | 0 |
|
||||||
|
|
||||||
|
### Settings App (11 endpoint)
|
||||||
|
|
||||||
|
| Kaynak | Public | Auth | Admin |
|
||||||
|
|--------|--------|------|-------|
|
||||||
|
| CORS Whitelist | 0 | 0 | 4 |
|
||||||
|
| CORS Blacklist | 0 | 0 | 4 |
|
||||||
|
| Rate Limits | 0 | 0 | 2 |
|
||||||
|
| Cache | 0 | 0 | 1 |
|
||||||
|
|
||||||
|
**TOPLAM: 57 endpoint**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## cURL Örnekleri
|
||||||
|
|
||||||
|
### Public Endpoint
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/v1/posts?page=1&limit=5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin Endpoint
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/admin/categories \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"title": "Test"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Endpoints
|
||||||
|
|
||||||
|
### Register
|
||||||
|
**POST** `/api/v1/auth/register`
|
||||||
|
Create a new user account.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "securePass123",
|
||||||
|
"username": "johndoe"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"email": "user@example.com",
|
||||||
|
"username": "johndoe",
|
||||||
|
"avatar": "",
|
||||||
|
"created_at": "2024-03-20T10:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login
|
||||||
|
**POST** `/api/v1/auth/login`
|
||||||
|
Authenticate user and get JWT token.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "securePass123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
"user": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### OAuth Login (Google)
|
||||||
|
**GET** `/api/v1/auth/google`
|
||||||
|
Redirects to Google OAuth login page.
|
||||||
|
|
||||||
|
**Callback:** `/api/v1/auth/google/callback`
|
||||||
|
Exchanges code for token and returns JWT + User.
|
||||||
|
|
||||||
|
### OAuth Login (GitHub)
|
||||||
|
**GET** `/api/v1/auth/github`
|
||||||
|
Redirects to GitHub OAuth login page.
|
||||||
|
|
||||||
|
**Callback:** `/api/v1/auth/github/callback`
|
||||||
|
Exchanges code for token and returns JWT + User.
|
||||||
|
|
||||||
|
### Logout
|
||||||
|
**POST** `/api/v1/auth/logout`
|
||||||
|
Logout user (client-side token removal recommended).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blog Endpoints
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/user/posts/1/comments \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{"body": "Great post!"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Postman Collection
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"base_url": "http://localhost:8080/api/v1",
|
||||||
|
"admin_token": "eyJhbGc...",
|
||||||
|
"user_token": "eyJhbGc..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Middleware Notları
|
||||||
|
|
||||||
|
Auth ve Admin middleware'ler şu anda yorumda:
|
||||||
|
|
||||||
|
**Aktif etmek için** [routes.go](file:///Users/beyhan/Desktop/Projeler/Go/gobeyhan/app/routes/routes.go):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Auth routes
|
||||||
|
user := api.Group("/user")
|
||||||
|
user.Use(AuthMiddleware())
|
||||||
|
|
||||||
|
// Admin routes
|
||||||
|
admin := api.Group("/admin")
|
||||||
|
admin.Use(AuthMiddleware(), AdminMiddleware())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Swagger Entegrasyonu
|
||||||
|
|
||||||
|
### Kurulum
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get -u github.com/swaggo/swag/cmd/swag
|
||||||
|
go get -u github.com/swaggo/gin-swagger
|
||||||
|
go get -u github.com/swaggo/files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
swag init
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erişim
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8080/swagger/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2026-02-11
|
||||||
|
**Version:** 2.0 (Modular Structure)
|
||||||
361
app/account/handlers/auth_handler.go
Normal file
361
app/account/handlers/auth_handler.go
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gobeyhan/app/account/services"
|
||||||
|
settingsServices "gobeyhan/app/settings/services"
|
||||||
|
"gobeyhan/config"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
userService *services.UserService
|
||||||
|
jwtService *settingsServices.JWTService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthHandler(userService *services.UserService, jwtService *settingsServices.JWTService) *AuthHandler {
|
||||||
|
return &AuthHandler{
|
||||||
|
userService: userService,
|
||||||
|
jwtService: jwtService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register godoc
|
||||||
|
// @Summary Register a new user
|
||||||
|
// @Description Create a new user account with email and password
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body object{email=string,password=string,username=string} true "Registration data"
|
||||||
|
// @Success 201 {object} object{token=string,user=models.User}
|
||||||
|
// @Failure 400 {object} object{error=string}
|
||||||
|
// @Router /api/v1/auth/register [post]
|
||||||
|
func (h *AuthHandler) Register(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required,min=6"`
|
||||||
|
Username string `json:"username" binding:"required,min=3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user (password will be hashed by UserService)
|
||||||
|
user := &models.User{
|
||||||
|
Email: input.Email,
|
||||||
|
UserName: input.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password is passed separately to be hashed
|
||||||
|
if err := h.userService.CreateUser(user, input.Password); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign default role
|
||||||
|
if err := h.userService.AssignDefaultRole(user.ID); err != nil {
|
||||||
|
// Log error but don't fail registration
|
||||||
|
// log.Printf("Failed to assign default role: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT tokens
|
||||||
|
accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return tokens and user (without password)
|
||||||
|
user.Password = ""
|
||||||
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
|
"access_token": accessToken,
|
||||||
|
"refresh_token": refreshToken,
|
||||||
|
"user": user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login godoc
|
||||||
|
// @Summary Login user
|
||||||
|
// @Description Login with email and password
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body object{email=string,password=string} true "Login credentials"
|
||||||
|
// @Success 200 {object} object{token=string,user=models.User}
|
||||||
|
// @Failure 400 {object} object{error=string}
|
||||||
|
// @Failure 401 {object} object{error=string}
|
||||||
|
// @Router /api/v1/auth/login [post]
|
||||||
|
func (h *AuthHandler) Login(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
TurnstileToken string `json:"turnstile_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Turnstile
|
||||||
|
if !verifyTurnstile(input.TurnstileToken) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Turnstile doğrulaması başarısız"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user by email
|
||||||
|
user, err := h.userService.GetUserByEmail(input.Email)
|
||||||
|
if err != nil || user == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
if !h.userService.VerifyPassword(user.Password, input.Password) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT tokens
|
||||||
|
accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set refresh token as HttpOnly cookie (secure, XSS-safe)
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "refresh_token",
|
||||||
|
Value: refreshToken,
|
||||||
|
Path: "/",
|
||||||
|
Domain: "localhost", // Explicitly set for local dev
|
||||||
|
MaxAge: 7 * 24 * 60 * 60, // 7 days
|
||||||
|
Secure: false, // Set true for HTTPS in production
|
||||||
|
HttpOnly: true, // Cannot be accessed by JavaScript
|
||||||
|
SameSite: http.SameSiteLaxMode, // Lax is better for local dev
|
||||||
|
}
|
||||||
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
fmt.Printf("[DEBUG] Login - Set-Cookie Raw: %s\n", cookie.String())
|
||||||
|
|
||||||
|
// Return tokens and user (without password)
|
||||||
|
user.Password = ""
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"access_token": accessToken,
|
||||||
|
"refresh_token": refreshToken, // Also in response for fallback
|
||||||
|
"user": user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentUser godoc
|
||||||
|
// @Summary Get current user
|
||||||
|
// @Description Get current authenticated user information
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 401 {object} object{error=string}
|
||||||
|
// @Router /api/v1/auth/me [get]
|
||||||
|
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
|
||||||
|
// Get user ID from context (set by auth middleware)
|
||||||
|
userIDStr, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to uint64
|
||||||
|
var userID uint64
|
||||||
|
switch v := userIDStr.(type) {
|
||||||
|
case string:
|
||||||
|
parsed, _ := strconv.ParseUint(v, 10, 64)
|
||||||
|
userID = parsed
|
||||||
|
case uint64:
|
||||||
|
userID = v
|
||||||
|
case int:
|
||||||
|
userID = uint64(v)
|
||||||
|
case float64:
|
||||||
|
userID = uint64(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user from database
|
||||||
|
user, err := h.userService.GetUserByID(userID)
|
||||||
|
if err != nil || user == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return user (without password)
|
||||||
|
user.Password = ""
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout godoc
|
||||||
|
// @Summary Logout user
|
||||||
|
// @Description Logout (client-side token removal)
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} object{message=string}
|
||||||
|
// @Router /api/v1/auth/logout [post]
|
||||||
|
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||||
|
// Clear refresh token cookie
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "refresh_token",
|
||||||
|
Value: "",
|
||||||
|
Path: "/",
|
||||||
|
Domain: "localhost",
|
||||||
|
MaxAge: -1, // Delete cookie
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
}
|
||||||
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
fmt.Printf("[DEBUG] Logout - Set-Cookie Raw: %s\n", cookie.String())
|
||||||
|
|
||||||
|
// For JWT, logout is typically handled client-side
|
||||||
|
// Server can implement token blacklisting if needed
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken godoc
|
||||||
|
// @Summary Refresh access token
|
||||||
|
// @Description Get a new access token using refresh token from HttpOnly cookie
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} object{access_token=string,refresh_token=string}
|
||||||
|
// @Failure 401 {object} object{error=string}
|
||||||
|
// @Router /api/v1/auth/refresh [post]
|
||||||
|
func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||||
|
// Get refresh token from HttpOnly cookie
|
||||||
|
refreshToken, err := c.Cookie("refresh_token")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[DEBUG] RefreshToken - Cookie Error: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[DEBUG] RefreshToken - Cookie Found: %s...\n", refreshToken[:10])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil || refreshToken == "" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Refresh token not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate refresh token and get user ID
|
||||||
|
claims, err := h.jwtService.ValidateToken(refreshToken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[DEBUG] RefreshToken - ValidateToken Error: %v\n", err)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired refresh token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user ID from claims
|
||||||
|
userID, err := claims.GetSubject()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[DEBUG] RefreshToken - GetSubject Error: %v\n", err)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse user ID to uint64
|
||||||
|
// Sscan bazen boşluk vs. yüzünden hata verebilir, strconv daha güvenli
|
||||||
|
parsedUint, err := strconv.ParseUint(userID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[DEBUG] RefreshToken - ParseUint Error: %v (userID=%s)\n", err, userID)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uid := parsedUint
|
||||||
|
|
||||||
|
// Get user from database
|
||||||
|
user, err := h.userService.GetUserByID(uid)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[DEBUG] RefreshToken - GetUserByID FAILED: %v\n", err)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Database error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
fmt.Printf("[DEBUG] RefreshToken - User not found in DB for ID: %d\n", uid)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("[DEBUG] RefreshToken - User found: %s\n", user.Email)
|
||||||
|
|
||||||
|
// Generate new token pair
|
||||||
|
newAccessToken, newRefreshToken, err := h.jwtService.GenerateTokenPair(*user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update refresh token cookie (token rotation)
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: "refresh_token",
|
||||||
|
Value: newRefreshToken,
|
||||||
|
Path: "/",
|
||||||
|
Domain: "localhost",
|
||||||
|
MaxAge: 7 * 24 * 60 * 60, // 7 days
|
||||||
|
Secure: false,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
}
|
||||||
|
http.SetCookie(c.Writer, cookie)
|
||||||
|
|
||||||
|
// Return new access token and user info
|
||||||
|
user.Password = "" // Ensure password is not sent
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"access_token": newAccessToken,
|
||||||
|
"refresh_token": newRefreshToken,
|
||||||
|
"user": user, // Critical for frontend session restore
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Verify Turnstile Token
|
||||||
|
func verifyTurnstile(token string) bool {
|
||||||
|
secret := config.AppConfig.TurnstileSecretKey
|
||||||
|
if secret == "" {
|
||||||
|
fmt.Println("[WARNING] Turnstile Secret Key not configured, skipping validation")
|
||||||
|
return true // Skip validation if not configured
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
// If secret is configured, token is mandatory
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
formData := url.Values{
|
||||||
|
"secret": {secret},
|
||||||
|
"response": {token},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", formData)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[ERROR] Turnstile request failed: %v\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[ERROR] Failed to read Turnstile response: %v\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
fmt.Printf("[ERROR] Failed to parse Turnstile response: %v\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Success
|
||||||
|
}
|
||||||
297
app/account/handlers/oauth_handler.go
Normal file
297
app/account/handlers/oauth_handler.go
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gobeyhan/app/account/services"
|
||||||
|
settingsServices "gobeyhan/app/settings/services"
|
||||||
|
"gobeyhan/config"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/github"
|
||||||
|
"golang.org/x/oauth2/google"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OAuthHandler struct {
|
||||||
|
userService *services.UserService
|
||||||
|
socialAccountService *services.SocialAccountService
|
||||||
|
jwtService *settingsServices.JWTService
|
||||||
|
googleOAuthConfig *oauth2.Config
|
||||||
|
githubOAuthConfig *oauth2.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOAuthHandler(
|
||||||
|
userService *services.UserService,
|
||||||
|
socialAccountService *services.SocialAccountService,
|
||||||
|
jwtService *settingsServices.JWTService,
|
||||||
|
) *OAuthHandler {
|
||||||
|
// Google OAuth config
|
||||||
|
googleConfig := &oauth2.Config{
|
||||||
|
ClientID: config.AppConfig.GoogleClientID,
|
||||||
|
ClientSecret: config.AppConfig.GoogleClientSecret,
|
||||||
|
RedirectURL: config.AppConfig.GoogleRedirectURL,
|
||||||
|
Scopes: []string{
|
||||||
|
"https://www.googleapis.com/auth/userinfo.email",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.profile",
|
||||||
|
},
|
||||||
|
Endpoint: google.Endpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitHub OAuth config
|
||||||
|
githubConfig := &oauth2.Config{
|
||||||
|
ClientID: config.AppConfig.GithubClientID,
|
||||||
|
ClientSecret: config.AppConfig.GithubClientSecret,
|
||||||
|
RedirectURL: config.AppConfig.GithubRedirectURL,
|
||||||
|
Scopes: []string{"user:email"},
|
||||||
|
Endpoint: github.Endpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OAuthHandler{
|
||||||
|
userService: userService,
|
||||||
|
socialAccountService: socialAccountService,
|
||||||
|
jwtService: jwtService,
|
||||||
|
googleOAuthConfig: googleConfig,
|
||||||
|
githubOAuthConfig: githubConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoogleLogin godoc
|
||||||
|
// @Summary Google OAuth login
|
||||||
|
// @Description Redirect to Google OAuth
|
||||||
|
// @Tags auth,oauth
|
||||||
|
// @Produce json
|
||||||
|
// @Router /api/v1/auth/google [get]
|
||||||
|
func (h *OAuthHandler) GoogleLogin(c *gin.Context) {
|
||||||
|
url := h.googleOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoogleCallback godoc
|
||||||
|
// @Summary Google OAuth callback
|
||||||
|
// @Description Handle Google OAuth callback
|
||||||
|
// @Tags auth,oauth
|
||||||
|
// @Produce json
|
||||||
|
// @Param code query string true "Authorization code"
|
||||||
|
// @Success 200 {object} object{token=string,user=models.User}
|
||||||
|
// @Failure 400 {object} object{error=string}
|
||||||
|
// @Router /api/v1/auth/google/callback [get]
|
||||||
|
func (h *OAuthHandler) GoogleCallback(c *gin.Context) {
|
||||||
|
code := c.Query("code")
|
||||||
|
if code == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Code not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange code for token
|
||||||
|
token, err := h.googleOAuthConfig.Exchange(context.Background(), code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user info from Google
|
||||||
|
client := h.googleOAuthConfig.Client(context.Background(), token)
|
||||||
|
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, _ := io.ReadAll(resp.Body)
|
||||||
|
var googleUser struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
VerifiedEmail bool `json:"verified_email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Picture string `json:"picture"`
|
||||||
|
}
|
||||||
|
json.Unmarshal(data, &googleUser)
|
||||||
|
|
||||||
|
// Find or create user
|
||||||
|
user, accessToken, refreshToken, err := h.findOrCreateOAuthUser(
|
||||||
|
googleUser.Email,
|
||||||
|
googleUser.Name,
|
||||||
|
"google",
|
||||||
|
googleUser.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"token": accessToken,
|
||||||
|
"refresh_token": refreshToken,
|
||||||
|
"user": user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GithubLogin godoc
|
||||||
|
// @Summary GitHub OAuth login
|
||||||
|
// @Description Redirect to GitHub OAuth
|
||||||
|
// @Tags auth,oauth
|
||||||
|
// @Produce json
|
||||||
|
// @Router /api/v1/auth/github [get]
|
||||||
|
func (h *OAuthHandler) GithubLogin(c *gin.Context) {
|
||||||
|
url := h.githubOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GithubCallback godoc
|
||||||
|
// @Summary GitHub OAuth callback
|
||||||
|
// @Description Handle GitHub OAuth callback
|
||||||
|
// @Tags auth,oauth
|
||||||
|
// @Produce json
|
||||||
|
// @Param code query string true "Authorization code"
|
||||||
|
// @Success 200 {object} object{token=string,user=models.User}
|
||||||
|
// @Failure 400 {object} object{error=string}
|
||||||
|
// @Router /api/v1/auth/github/callback [get]
|
||||||
|
func (h *OAuthHandler) GithubCallback(c *gin.Context) {
|
||||||
|
code := c.Query("code")
|
||||||
|
if code == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Code not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange code for token
|
||||||
|
token, err := h.githubOAuthConfig.Exchange(context.Background(), code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user info from GitHub
|
||||||
|
client := h.githubOAuthConfig.Client(context.Background(), token)
|
||||||
|
resp, err := client.Get("https://api.github.com/user")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, _ := io.ReadAll(resp.Body)
|
||||||
|
var githubUser struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
json.Unmarshal(data, &githubUser)
|
||||||
|
|
||||||
|
// If email is not public, fetch it separately
|
||||||
|
if githubUser.Email == "" {
|
||||||
|
emailResp, _ := client.Get("https://api.github.com/user/emails")
|
||||||
|
if emailResp != nil {
|
||||||
|
defer emailResp.Body.Close()
|
||||||
|
emailData, _ := io.ReadAll(emailResp.Body)
|
||||||
|
var emails []struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Primary bool `json:"primary"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
}
|
||||||
|
json.Unmarshal(emailData, &emails)
|
||||||
|
for _, e := range emails {
|
||||||
|
if e.Primary && e.Verified {
|
||||||
|
githubUser.Email = e.Email
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
username := githubUser.Name
|
||||||
|
if username == "" {
|
||||||
|
username = githubUser.Login
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create user
|
||||||
|
user, accessToken, refreshToken, err := h.findOrCreateOAuthUser(
|
||||||
|
githubUser.Email,
|
||||||
|
username,
|
||||||
|
"github",
|
||||||
|
strconv.Itoa(githubUser.ID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"token": accessToken,
|
||||||
|
"refresh_token": refreshToken,
|
||||||
|
"user": user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// findOrCreateOAuthUser finds existing user or creates new one for OAuth
|
||||||
|
func (h *OAuthHandler) findOrCreateOAuthUser(
|
||||||
|
email, username, provider, providerUserID string,
|
||||||
|
) (*models.User, string, string, error) {
|
||||||
|
// Try to find existing user by email
|
||||||
|
user, err := h.userService.GetUserByEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user doesn't exist, create new one
|
||||||
|
if user == nil {
|
||||||
|
user = &models.User{
|
||||||
|
Email: email,
|
||||||
|
UserName: username,
|
||||||
|
}
|
||||||
|
// Create user with empty password
|
||||||
|
if err := h.userService.CreateUser(user, ""); err != nil {
|
||||||
|
return nil, "", "", fmt.Errorf("failed to create user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign default role
|
||||||
|
if err := h.userService.AssignDefaultRole(user.ID); err != nil {
|
||||||
|
// Log error but continue
|
||||||
|
// fmt.Printf("Failed to assign default role: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if social account exists
|
||||||
|
accounts, err := h.socialAccountService.GetSocialAccountsByUser(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create social account if it doesn't exist
|
||||||
|
found := false
|
||||||
|
for _, acc := range accounts {
|
||||||
|
if acc.Provider == provider && acc.ProviderID == providerUserID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
socialAccount := &models.SocialAccount{
|
||||||
|
UserID: user.ID,
|
||||||
|
Provider: provider,
|
||||||
|
ProviderID: providerUserID,
|
||||||
|
}
|
||||||
|
if err := h.socialAccountService.CreateSocialAccount(socialAccount); err != nil {
|
||||||
|
return nil, "", "", fmt.Errorf("failed to create social account: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT tokens
|
||||||
|
accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", "", fmt.Errorf("failed to generate tokens: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear password before returning
|
||||||
|
user.Password = ""
|
||||||
|
return user, accessToken, refreshToken, nil
|
||||||
|
}
|
||||||
70
app/account/handlers/permission_handler.go
Normal file
70
app/account/handlers/permission_handler.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/account/services"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PermissionHandler struct {
|
||||||
|
service *services.PermissionService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPermissionHandler(service *services.PermissionService) *PermissionHandler {
|
||||||
|
return &PermissionHandler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetAllPermissions godoc
|
||||||
|
// @Summary Get all permissions (Admin)
|
||||||
|
// @Description Get list of all permissions
|
||||||
|
// @Tags admin,permissions
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {array} models.Permission
|
||||||
|
// @Router /api/v1/admin/permissions [get]
|
||||||
|
func (h *PermissionHandler) AdminGetAllPermissions(c *gin.Context) {
|
||||||
|
permissions, err := h.service.GetAllPermissions()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": permissions})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminCreatePermission godoc
|
||||||
|
// @Summary Create a new permission (Admin)
|
||||||
|
// @Description Create a new permission
|
||||||
|
// @Tags admin,permissions
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param permission body models.Permission true "Permission object"
|
||||||
|
// @Success 201 {object} models.Permission
|
||||||
|
// @Router /api/v1/admin/permissions [post]
|
||||||
|
func (h *PermissionHandler) AdminCreatePermission(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
permission := &models.Permission{
|
||||||
|
Name: input.Name,
|
||||||
|
Description: input.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreatePermission(permission); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"data": permission})
|
||||||
|
}
|
||||||
169
app/account/handlers/role_handler.go
Normal file
169
app/account/handlers/role_handler.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/account/services"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoleHandler struct {
|
||||||
|
service *services.RoleService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRoleHandler(service *services.RoleService) *RoleHandler {
|
||||||
|
return &RoleHandler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetAllRoles godoc
|
||||||
|
// @Summary Get all roles (Admin)
|
||||||
|
// @Description Get list of all roles with permissions
|
||||||
|
// @Tags admin,roles
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {array} models.Role
|
||||||
|
// @Router /api/v1/admin/roles [get]
|
||||||
|
func (h *RoleHandler) AdminGetAllRoles(c *gin.Context) {
|
||||||
|
roles, err := h.service.GetAllRoles()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": roles})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetRoleByID godoc
|
||||||
|
// @Summary Get role by ID (Admin)
|
||||||
|
// @Description Get a single role by ID
|
||||||
|
// @Tags admin,roles
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Role ID"
|
||||||
|
// @Success 200 {object} models.Role
|
||||||
|
// @Router /api/v1/admin/roles/{id} [get]
|
||||||
|
func (h *RoleHandler) AdminGetRoleByID(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
role, err := h.service.GetRoleByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if role == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Role not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": role})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminCreateRole godoc
|
||||||
|
// @Summary Create a new role (Admin)
|
||||||
|
// @Description Create a new role
|
||||||
|
// @Tags admin,roles
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param role body models.Role true "Role object"
|
||||||
|
// @Success 201 {object} models.Role
|
||||||
|
// @Router /api/v1/admin/roles [post]
|
||||||
|
func (h *RoleHandler) AdminCreateRole(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
role := &models.Role{
|
||||||
|
Name: input.Name,
|
||||||
|
Description: input.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreateRole(role); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"data": role})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUpdateRole godoc
|
||||||
|
// @Summary Update a role (Admin)
|
||||||
|
// @Description Update an existing role
|
||||||
|
// @Tags admin,roles
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Role ID"
|
||||||
|
// @Param role body models.Role true "Role object"
|
||||||
|
// @Success 200 {object} models.Role
|
||||||
|
// @Router /api/v1/admin/roles/{id} [put]
|
||||||
|
func (h *RoleHandler) AdminUpdateRole(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateRole(id, input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch updated role
|
||||||
|
role, err := h.service.GetRoleByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": role})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminDeleteRole godoc
|
||||||
|
// @Summary Delete a role (Admin)
|
||||||
|
// @Description Delete a role by ID
|
||||||
|
// @Tags admin,roles
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Role ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/roles/{id} [delete]
|
||||||
|
func (h *RoleHandler) AdminDeleteRole(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.DeleteRole(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Role deleted successfully"})
|
||||||
|
}
|
||||||
111
app/account/handlers/social_account_handler.go
Normal file
111
app/account/handlers/social_account_handler.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/account/services"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SocialAccountHandler struct {
|
||||||
|
service *services.SocialAccountService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSocialAccountHandler(service *services.SocialAccountService) *SocialAccountHandler {
|
||||||
|
return &SocialAccountHandler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserSocialAccounts godoc
|
||||||
|
// @Summary Get user's social accounts
|
||||||
|
// @Description Get all social accounts for the authenticated user
|
||||||
|
// @Tags social-accounts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {array} models.SocialAccount
|
||||||
|
// @Router /api/v1/user/social-accounts [get]
|
||||||
|
func (h *SocialAccountHandler) GetUserSocialAccounts(c *gin.Context) {
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert user_id to uint64
|
||||||
|
var uid uint64
|
||||||
|
switch v := userID.(type) {
|
||||||
|
case string:
|
||||||
|
uid, _ = strconv.ParseUint(v, 10, 64)
|
||||||
|
case uint64:
|
||||||
|
uid = v
|
||||||
|
case int:
|
||||||
|
uid = uint64(v)
|
||||||
|
case float64:
|
||||||
|
uid = uint64(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts, err := h.service.GetSocialAccountsByUser(uid)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": accounts})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSocialAccount godoc
|
||||||
|
// @Summary Delete a social account
|
||||||
|
// @Description Delete a social account for the authenticated user
|
||||||
|
// @Tags social-accounts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Social Account ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/user/social-accounts/{id} [delete]
|
||||||
|
func (h *SocialAccountHandler) DeleteSocialAccount(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid social account ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ownership
|
||||||
|
account, err := h.service.GetSocialAccountByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if account == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Social account not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, _ := c.Get("user_id")
|
||||||
|
var uid uint64
|
||||||
|
switch v := userID.(type) {
|
||||||
|
case string:
|
||||||
|
uid, _ = strconv.ParseUint(v, 10, 64)
|
||||||
|
case uint64:
|
||||||
|
uid = v
|
||||||
|
case int:
|
||||||
|
uid = uint64(v)
|
||||||
|
case float64:
|
||||||
|
uid = uint64(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.UserID != uid {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "You can only delete your own social accounts"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.DeleteSocialAccount(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Social account deleted successfully"})
|
||||||
|
}
|
||||||
287
app/account/handlers/user_handler.go
Normal file
287
app/account/handlers/user_handler.go
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/account/services"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserHandler struct {
|
||||||
|
service *services.UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserHandler(service *services.UserService) *UserHandler {
|
||||||
|
return &UserHandler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetAllUsers godoc
|
||||||
|
// @Summary Get all users (Admin)
|
||||||
|
// @Description Get paginated list of all users
|
||||||
|
// @Tags admin,users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param page query int false "Page number" default(1)
|
||||||
|
// @Param limit query int false "Items per page" default(10)
|
||||||
|
// @Param include_deleted query bool false "Include soft-deleted users"
|
||||||
|
// @Success 200 {object} map[string]interface{}
|
||||||
|
// @Router /api/v1/admin/users [get]
|
||||||
|
func (h *UserHandler) AdminGetAllUsers(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||||
|
includeDeleted := c.DefaultQuery("include_deleted", "false") == "true"
|
||||||
|
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit < 1 || limit > 100 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
users, total, err := h.service.GetAllUsers(includeDeleted, page, limit)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": users,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetUserByID godoc
|
||||||
|
// @Summary Get user by ID (Admin)
|
||||||
|
// @Description Get a single user by ID
|
||||||
|
// @Tags admin,users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "User ID"
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Router /api/v1/admin/users/{id} [get]
|
||||||
|
func (h *UserHandler) AdminGetUserByID(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.service.GetUserByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": user})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminCreateUser godoc
|
||||||
|
// @Summary Create a new user (Admin)
|
||||||
|
// @Description Create a new user
|
||||||
|
// @Tags admin,users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param user body models.User true "User object"
|
||||||
|
// @Success 201 {object} models.User
|
||||||
|
// @Router /api/v1/admin/users [post]
|
||||||
|
func (h *UserHandler) AdminCreateUser(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
UserName string `json:"username"`
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
UserName: input.UserName,
|
||||||
|
Email: input.Email,
|
||||||
|
Avatar: input.Avatar,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreateUser(user, input.Password); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"data": user})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUpdateUser godoc
|
||||||
|
// @Summary Update a user (Admin)
|
||||||
|
// @Description Update an existing user
|
||||||
|
// @Tags admin,users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "User ID"
|
||||||
|
// @Param user body models.User true "User object"
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Router /api/v1/admin/users/{id} [put]
|
||||||
|
func (h *UserHandler) AdminUpdateUser(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateUser(id, input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch updated user
|
||||||
|
user, err := h.service.GetUserByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": user})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminDeleteUser godoc
|
||||||
|
// @Summary Delete a user (Admin)
|
||||||
|
// @Description Soft delete a user by ID
|
||||||
|
// @Tags admin,users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "User ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/users/{id} [delete]
|
||||||
|
func (h *UserHandler) AdminDeleteUser(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.DeleteUser(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminRestoreUser godoc
|
||||||
|
// @Summary Restore a deleted user (Admin)
|
||||||
|
// @Description Restore a soft-deleted user
|
||||||
|
// @Tags admin,users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "User ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/users/{id}/restore [post]
|
||||||
|
func (h *UserHandler) AdminRestoreUser(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.RestoreUser(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "User restored successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminAssignRole godoc
|
||||||
|
// @Summary Assign role to user (Admin)
|
||||||
|
// @Description Assign a role to a user
|
||||||
|
// @Tags admin,users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "User ID"
|
||||||
|
// @Param role_id body int true "Role ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/users/{id}/roles [post]
|
||||||
|
func (h *UserHandler) AdminAssignRole(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
userID, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input struct {
|
||||||
|
RoleID uint64 `json:"role_id" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.AssignRole(userID, input.RoleID); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Role assigned successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminRemoveRole godoc
|
||||||
|
// @Summary Remove role from user (Admin)
|
||||||
|
// @Description Remove a role from a user
|
||||||
|
// @Tags admin,users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "User ID"
|
||||||
|
// @Param role_id path int true "Role ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/users/{id}/roles/{role_id} [delete]
|
||||||
|
func (h *UserHandler) AdminRemoveRole(c *gin.Context) {
|
||||||
|
userIDStr := c.Param("id")
|
||||||
|
userID, err := strconv.ParseUint(userIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
roleIDStr := c.Param("role_id")
|
||||||
|
roleID, err := strconv.ParseUint(roleIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.RemoveRole(userID, roleID); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Role removed successfully"})
|
||||||
|
}
|
||||||
42
app/account/services/permission_service.go
Normal file
42
app/account/services/permission_service.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PermissionService struct{}
|
||||||
|
|
||||||
|
func NewPermissionService() *PermissionService {
|
||||||
|
return &PermissionService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllPermissions retrieves all permissions
|
||||||
|
func (s *PermissionService) GetAllPermissions() ([]models.Permission, error) {
|
||||||
|
var permissions []models.Permission
|
||||||
|
err := database.DB.Find(&permissions).Error
|
||||||
|
return permissions, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPermissionByID retrieves a permission by ID
|
||||||
|
func (s *PermissionService) GetPermissionByID(id uint64) (*models.Permission, error) {
|
||||||
|
var permission models.Permission
|
||||||
|
err := database.DB.First(&permission, id).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &permission, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePermission creates a new permission
|
||||||
|
func (s *PermissionService) CreatePermission(permission *models.Permission) error {
|
||||||
|
return database.DB.Create(permission).Error
|
||||||
|
}
|
||||||
96
app/account/services/role_service.go
Normal file
96
app/account/services/role_service.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoleService struct{}
|
||||||
|
|
||||||
|
func NewRoleService() *RoleService {
|
||||||
|
return &RoleService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllRoles retrieves all roles
|
||||||
|
func (s *RoleService) GetAllRoles() ([]models.Role, error) {
|
||||||
|
var roles []models.Role
|
||||||
|
err := database.DB.Preload("Permissions").Find(&roles).Error
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoleByID retrieves a role by ID
|
||||||
|
func (s *RoleService) GetRoleByID(id uint64) (*models.Role, error) {
|
||||||
|
var role models.Role
|
||||||
|
err := database.DB.Preload("Permissions").First(&role, id).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &role, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRole creates a new role
|
||||||
|
func (s *RoleService) CreateRole(role *models.Role) error {
|
||||||
|
return database.DB.Create(role).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRole updates an existing role
|
||||||
|
func (s *RoleService) UpdateRole(id uint64, updates map[string]interface{}) error {
|
||||||
|
result := database.DB.Model(&models.Role{}).Where("id = ?", id).Updates(updates)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRole deletes a role by ID
|
||||||
|
func (s *RoleService) DeleteRole(id uint64) error {
|
||||||
|
result := database.DB.Delete(&models.Role{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignPermission assigns a permission to a role
|
||||||
|
func (s *RoleService) AssignPermission(roleID, permissionID uint64) error {
|
||||||
|
var role models.Role
|
||||||
|
if err := database.DB.First(&role, roleID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var permission models.Permission
|
||||||
|
if err := database.DB.First(&permission, permissionID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.DB.Model(&role).Association("Permissions").Append(&permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePermission removes a permission from a role
|
||||||
|
func (s *RoleService) RemovePermission(roleID, permissionID uint64) error {
|
||||||
|
var role models.Role
|
||||||
|
if err := database.DB.Preload("Permissions").First(&role, roleID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var permission models.Permission
|
||||||
|
if err := database.DB.First(&permission, permissionID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.DB.Model(&role).Association("Permissions").Delete(&permission)
|
||||||
|
}
|
||||||
54
app/account/services/social_account_service.go
Normal file
54
app/account/services/social_account_service.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SocialAccountService struct{}
|
||||||
|
|
||||||
|
func NewSocialAccountService() *SocialAccountService {
|
||||||
|
return &SocialAccountService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSocialAccountsByUser retrieves all social accounts for a user
|
||||||
|
func (s *SocialAccountService) GetSocialAccountsByUser(userID uint64) ([]models.SocialAccount, error) {
|
||||||
|
var accounts []models.SocialAccount
|
||||||
|
err := database.DB.Where("user_id = ?", userID).Find(&accounts).Error
|
||||||
|
return accounts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSocialAccountByID retrieves a social account by ID
|
||||||
|
func (s *SocialAccountService) GetSocialAccountByID(id uint64) (*models.SocialAccount, error) {
|
||||||
|
var account models.SocialAccount
|
||||||
|
err := database.DB.First(&account, id).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSocialAccount creates a new social account
|
||||||
|
func (s *SocialAccountService) CreateSocialAccount(account *models.SocialAccount) error {
|
||||||
|
return database.DB.Create(account).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSocialAccount deletes a social account by ID
|
||||||
|
func (s *SocialAccountService) DeleteSocialAccount(id uint64) error {
|
||||||
|
result := database.DB.Delete(&models.SocialAccount{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
184
app/account/services/user_service.go
Normal file
184
app/account/services/user_service.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserService struct{}
|
||||||
|
|
||||||
|
func NewUserService() *UserService {
|
||||||
|
return &UserService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllUsers retrieves all users, optionally including soft-deleted ones
|
||||||
|
func (s *UserService) GetAllUsers(includeDeleted bool, page, limit int) ([]models.User, int64, error) {
|
||||||
|
var users []models.User
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := database.DB.Preload("Roles").Preload("SocialAccounts")
|
||||||
|
|
||||||
|
if includeDeleted {
|
||||||
|
query = query.Unscoped()
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Model(&models.User{}).Count(&total)
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Offset((page - 1) * limit).
|
||||||
|
Limit(limit).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&users).Error
|
||||||
|
|
||||||
|
return users, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByID retrieves a user by ID
|
||||||
|
func (s *UserService) GetUserByID(id uint64) (*models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
err := database.DB.
|
||||||
|
Preload("Roles").
|
||||||
|
Preload("SocialAccounts").
|
||||||
|
First(&user, id).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail retrieves a user by email
|
||||||
|
func (s *UserService) GetUserByEmail(email string) (*models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
err := database.DB.
|
||||||
|
Preload("Roles").
|
||||||
|
Preload("SocialAccounts").
|
||||||
|
Where("email = ?", email).
|
||||||
|
First(&user).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user with hashed password
|
||||||
|
func (s *UserService) CreateUser(user *models.User, password string) error {
|
||||||
|
// Hash password if provided
|
||||||
|
if password != "" {
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.Password = string(hashedPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.DB.Create(user).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPassword checks if the provided password matches the hashed password
|
||||||
|
func (s *UserService) VerifyPassword(hashedPassword, password string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser updates an existing user
|
||||||
|
func (s *UserService) UpdateUser(id uint64, updates map[string]interface{}) error {
|
||||||
|
// If password is being updated, hash it first
|
||||||
|
if password, ok := updates["password"].(string); ok && password != "" {
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updates["password"] = string(hashedPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := database.DB.Model(&models.User{}).Where("id = ?", id).Updates(updates)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser soft deletes a user
|
||||||
|
func (s *UserService) DeleteUser(id uint64) error {
|
||||||
|
result := database.DB.Delete(&models.User{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreUser restores a soft-deleted user
|
||||||
|
func (s *UserService) RestoreUser(id uint64) error {
|
||||||
|
result := database.DB.Model(&models.User{}).Unscoped().
|
||||||
|
Where("id = ?", id).
|
||||||
|
Update("deleted_at", nil)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignRole assigns a role to a user
|
||||||
|
func (s *UserService) AssignRole(userID, roleID uint64) error {
|
||||||
|
var user models.User
|
||||||
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var role models.Role
|
||||||
|
if err := database.DB.First(&role, roleID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.DB.Model(&user).Association("Roles").Append(&role)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRole removes a role from a user
|
||||||
|
func (s *UserService) RemoveRole(userID, roleID uint64) error {
|
||||||
|
var user models.User
|
||||||
|
if err := database.DB.Preload("Roles").First(&user, userID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var role models.Role
|
||||||
|
if err := database.DB.First(&role, roleID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.DB.Model(&user).Association("Roles").Delete(&role)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignDefaultRole assigns the default 'user' role to a user
|
||||||
|
func (s *UserService) AssignDefaultRole(userID uint64) error {
|
||||||
|
var role models.Role
|
||||||
|
// Find role by name 'user'
|
||||||
|
if err := database.DB.Where("name = ?", "user").First(&role).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.AssignRole(userID, role.ID)
|
||||||
|
}
|
||||||
235
app/blog/handlers/category_handler.go
Normal file
235
app/blog/handlers/category_handler.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/blog/services"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CategoryHandler struct {
|
||||||
|
service *services.CategoryService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCategoryHandler(service *services.CategoryService) *CategoryHandler {
|
||||||
|
return &CategoryHandler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCategories godoc
|
||||||
|
// @Summary Get all active categories
|
||||||
|
// @Description Get list of all active categories (public endpoint)
|
||||||
|
// @Tags categories
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} models.Category
|
||||||
|
// @Router /api/v1/categories [get]
|
||||||
|
func (h *CategoryHandler) GetAllCategories(c *gin.Context) {
|
||||||
|
categories, err := h.service.GetAllCategories(true)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": categories})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoryBySlug godoc
|
||||||
|
// @Summary Get category by slug
|
||||||
|
// @Description Get a single category by its slug (public endpoint)
|
||||||
|
// @Tags categories
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param slug path string true "Category Slug"
|
||||||
|
// @Success 200 {object} models.Category
|
||||||
|
// @Router /api/v1/categories/{slug} [get]
|
||||||
|
func (h *CategoryHandler) GetCategoryBySlug(c *gin.Context) {
|
||||||
|
slug := c.Param("slug")
|
||||||
|
|
||||||
|
category, err := h.service.GetCategoryBySlug(slug)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if category == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": category})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetAllCategories godoc
|
||||||
|
// @Summary Get all categories (Admin)
|
||||||
|
// @Description Get list of all categories including inactive ones
|
||||||
|
// @Tags admin,categories
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {array} models.Category
|
||||||
|
// @Router /api/v1/admin/categories [get]
|
||||||
|
func (h *CategoryHandler) AdminGetAllCategories(c *gin.Context) {
|
||||||
|
categories, err := h.service.GetAllCategories(false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": categories})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoryByID godoc
|
||||||
|
// @Summary Get category by ID (Admin)
|
||||||
|
// @Description Get a single category by ID
|
||||||
|
// @Tags admin,categories
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Category ID"
|
||||||
|
// @Success 200 {object} models.Category
|
||||||
|
// @Router /api/v1/admin/categories/{id} [get]
|
||||||
|
func (h *CategoryHandler) GetCategoryByID(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
category, err := h.service.GetCategoryByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if category == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": category})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCategory godoc
|
||||||
|
// @Summary Create a new category (Admin)
|
||||||
|
// @Description Create a new category
|
||||||
|
// @Tags admin,categories
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param category body models.Category true "Category object"
|
||||||
|
// @Success 201 {object} models.Category
|
||||||
|
// @Router /api/v1/admin/categories [post]
|
||||||
|
func (h *CategoryHandler) CreateCategory(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
Title string `json:"title" binding:"required"`
|
||||||
|
Keywords string `json:"keywords"`
|
||||||
|
Desc string `json:"description"`
|
||||||
|
IsActive *bool `json:"is_active"`
|
||||||
|
Order *int `json:"order"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
ParentID *uint64 `json:"parent_id"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
category := &models.Category{
|
||||||
|
Title: input.Title,
|
||||||
|
Keywords: input.Keywords,
|
||||||
|
Desc: input.Desc,
|
||||||
|
Slug: input.Slug,
|
||||||
|
ParentID: input.ParentID,
|
||||||
|
Image: input.Image,
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.IsActive != nil {
|
||||||
|
category.IsActive = *input.IsActive
|
||||||
|
} else {
|
||||||
|
category.IsActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Order != nil {
|
||||||
|
category.Order = *input.Order
|
||||||
|
} else {
|
||||||
|
category.Order = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreateCategory(category); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"data": category})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCategory godoc
|
||||||
|
// @Summary Update a category (Admin)
|
||||||
|
// @Description Update an existing category
|
||||||
|
// @Tags admin,categories
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Category ID"
|
||||||
|
// @Param category body models.Category true "Category object"
|
||||||
|
// @Success 200 {object} models.Category
|
||||||
|
// @Router /api/v1/admin/categories/{id} [put]
|
||||||
|
func (h *CategoryHandler) UpdateCategory(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateCategory(id, input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch updated category
|
||||||
|
category, err := h.service.GetCategoryByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": category})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCategory godoc
|
||||||
|
// @Summary Delete a category (Admin)
|
||||||
|
// @Description Delete a category by ID
|
||||||
|
// @Tags admin,categories
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Category ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/categories/{id} [delete]
|
||||||
|
func (h *CategoryHandler) DeleteCategory(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.DeleteCategory(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Category deleted successfully"})
|
||||||
|
}
|
||||||
111
app/blog/handlers/category_view_handler.go
Normal file
111
app/blog/handlers/category_view_handler.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/blog/services"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CategoryViewHandler struct {
|
||||||
|
service *services.CategoryViewService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCategoryViewHandler(service *services.CategoryViewService) *CategoryViewHandler {
|
||||||
|
return &CategoryViewHandler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackCategoryView godoc
|
||||||
|
// @Summary Track a category view
|
||||||
|
// @Description Record a view event for a category (public endpoint)
|
||||||
|
// @Tags category-views
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Category ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/categories/{id}/view [post]
|
||||||
|
func (h *CategoryViewHandler) TrackCategoryView(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
categoryID, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAddress := c.ClientIP()
|
||||||
|
userAgent := c.Request.UserAgent()
|
||||||
|
|
||||||
|
if err := h.service.TrackCategoryView(categoryID, ipAddress, userAgent); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "View tracked successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetAllCategoryViews godoc
|
||||||
|
// @Summary Get all category views (Admin)
|
||||||
|
// @Description Get paginated list of all category views
|
||||||
|
// @Tags admin,category-views
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param page query int false "Page number" default(1)
|
||||||
|
// @Param limit query int false "Items per page" default(10)
|
||||||
|
// @Success 200 {object} map[string]interface{}
|
||||||
|
// @Router /api/v1/admin/category-views [get]
|
||||||
|
func (h *CategoryViewHandler) AdminGetAllCategoryViews(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||||
|
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit < 1 || limit > 100 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
views, total, err := h.service.GetAllCategoryViews(page, limit)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": views,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoryViewStats godoc
|
||||||
|
// @Summary Get view stats for a category (Admin)
|
||||||
|
// @Description Get view count and details for a specific category
|
||||||
|
// @Tags admin,category-views
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Category ID"
|
||||||
|
// @Success 200 {object} map[string]interface{}
|
||||||
|
// @Router /api/v1/admin/categories/{id}/views [get]
|
||||||
|
func (h *CategoryViewHandler) GetCategoryViewStats(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
categoryID, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := h.service.GetCategoryViewCount(categoryID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"category_id": categoryID,
|
||||||
|
"view_count": count,
|
||||||
|
})
|
||||||
|
}
|
||||||
245
app/blog/handlers/comment_handler.go
Normal file
245
app/blog/handlers/comment_handler.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/blog/services"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommentHandler struct {
|
||||||
|
service *services.CommentService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommentHandler(service *services.CommentService) *CommentHandler {
|
||||||
|
return &CommentHandler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPostComments godoc
|
||||||
|
// @Summary Get comments for a post
|
||||||
|
// @Description Get all active comments for a specific post (public endpoint)
|
||||||
|
// @Tags comments
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Post ID"
|
||||||
|
// @Success 200 {array} models.Comment
|
||||||
|
// @Router /api/v1/posts/{id}/comments [get]
|
||||||
|
func (h *CommentHandler) GetPostComments(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
postID, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comments, err := h.service.GetCommentsByPost(postID, true)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": comments})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePostComment godoc
|
||||||
|
// @Summary Create a comment on a post
|
||||||
|
// @Description Create a new comment (requires authentication)
|
||||||
|
// @Tags comments
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Post ID"
|
||||||
|
// @Param comment body models.Comment true "Comment object"
|
||||||
|
// @Success 201 {object} models.Comment
|
||||||
|
// @Router /api/v1/posts/{id}/comments [post]
|
||||||
|
func (h *CommentHandler) CreatePostComment(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
postID, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user ID from context (set by auth middleware)
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body" binding:"required"`
|
||||||
|
ParentID *uint64 `json:"parent_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert user_id to uint64
|
||||||
|
var uid uint64
|
||||||
|
switch v := userID.(type) {
|
||||||
|
case string:
|
||||||
|
uid, _ = strconv.ParseUint(v, 10, 64)
|
||||||
|
case uint64:
|
||||||
|
uid = v
|
||||||
|
case int:
|
||||||
|
uid = uint64(v)
|
||||||
|
case float64:
|
||||||
|
uid = uint64(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
comment := &models.Comment{
|
||||||
|
UserID: uid,
|
||||||
|
ProductID: postID,
|
||||||
|
Title: input.Title,
|
||||||
|
Body: input.Body,
|
||||||
|
ParentID: input.ParentID,
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreateComment(comment); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"data": comment})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetAllComments godoc
|
||||||
|
// @Summary Get all comments (Admin)
|
||||||
|
// @Description Get paginated list of all comments
|
||||||
|
// @Tags admin,comments
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param page query int false "Page number" default(1)
|
||||||
|
// @Param limit query int false "Items per page" default(10)
|
||||||
|
// @Success 200 {object} map[string]interface{}
|
||||||
|
// @Router /api/v1/admin/comments [get]
|
||||||
|
func (h *CommentHandler) AdminGetAllComments(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||||
|
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit < 1 || limit > 100 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
comments, total, err := h.service.GetAllComments(page, limit, false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": comments,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetCommentByID godoc
|
||||||
|
// @Summary Get comment by ID (Admin)
|
||||||
|
// @Description Get a single comment by ID
|
||||||
|
// @Tags admin,comments
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Comment ID"
|
||||||
|
// @Success 200 {object} models.Comment
|
||||||
|
// @Router /api/v1/admin/comments/{id} [get]
|
||||||
|
func (h *CommentHandler) AdminGetCommentByID(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid comment ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comment, err := h.service.GetCommentByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Comment not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": comment})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUpdateComment godoc
|
||||||
|
// @Summary Update a comment (Admin)
|
||||||
|
// @Description Update an existing comment
|
||||||
|
// @Tags admin,comments
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Comment ID"
|
||||||
|
// @Param comment body models.Comment true "Comment object"
|
||||||
|
// @Success 200 {object} models.Comment
|
||||||
|
// @Router /api/v1/admin/comments/{id} [put]
|
||||||
|
func (h *CommentHandler) AdminUpdateComment(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid comment ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateComment(id, input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch updated comment
|
||||||
|
comment, err := h.service.GetCommentByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": comment})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminDeleteComment godoc
|
||||||
|
// @Summary Delete a comment (Admin)
|
||||||
|
// @Description Delete a comment by ID
|
||||||
|
// @Tags admin,comments
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Comment ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/comments/{id} [delete]
|
||||||
|
func (h *CommentHandler) AdminDeleteComment(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid comment ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.DeleteComment(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Comment deleted successfully"})
|
||||||
|
}
|
||||||
306
app/blog/handlers/post_handler.go
Normal file
306
app/blog/handlers/post_handler.go
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/blog/services"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostHandler struct {
|
||||||
|
service *services.PostService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostHandler(service *services.PostService) *PostHandler {
|
||||||
|
return &PostHandler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllPosts godoc
|
||||||
|
// @Summary Get all active posts
|
||||||
|
// @Description Get paginated list of active posts (public endpoint)
|
||||||
|
// @Tags posts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param page query int false "Page number" default(1)
|
||||||
|
// @Param limit query int false "Items per page" default(10)
|
||||||
|
// @Success 200 {object} map[string]interface{}
|
||||||
|
// @Router /api/v1/posts [get]
|
||||||
|
func (h *PostHandler) GetAllPosts(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||||
|
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit < 1 || limit > 100 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, total, err := h.service.GetAllPosts(page, limit, true)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": posts,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPostBySlug godoc
|
||||||
|
// @Summary Get post by slug
|
||||||
|
// @Description Get a single post by its slug (public endpoint)
|
||||||
|
// @Tags posts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param slug path string true "Post Slug"
|
||||||
|
// @Success 200 {object} models.Post
|
||||||
|
// @Router /api/v1/posts/{slug} [get]
|
||||||
|
func (h *PostHandler) GetPostBySlug(c *gin.Context) {
|
||||||
|
slug := c.Param("slug")
|
||||||
|
|
||||||
|
post, err := h.service.GetPostBySlug(slug)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if post == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": post})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetAllPosts godoc
|
||||||
|
// @Summary Get all posts (Admin)
|
||||||
|
// @Description Get paginated list of all posts including inactive
|
||||||
|
// @Tags admin,posts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param page query int false "Page number" default(1)
|
||||||
|
// @Param limit query int false "Items per page" default(10)
|
||||||
|
// @Success 200 {object} map[string]interface{}
|
||||||
|
// @Router /api/v1/admin/posts [get]
|
||||||
|
func (h *PostHandler) AdminGetAllPosts(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||||
|
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit < 1 || limit > 100 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, total, err := h.service.GetAllPosts(page, limit, false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"data": posts,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPostByID godoc
|
||||||
|
// @Summary Get post by ID (Admin)
|
||||||
|
// @Description Get a single post by ID
|
||||||
|
// @Tags admin,posts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Post ID"
|
||||||
|
// @Success 200 {object} models.Post
|
||||||
|
// @Router /api/v1/admin/posts/{id} [get]
|
||||||
|
func (h *PostHandler) GetPostByID(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := h.service.GetPostByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if post == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": post})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePost godoc
|
||||||
|
// @Summary Create a new post (Admin)
|
||||||
|
// @Description Create a new post
|
||||||
|
// @Tags admin,posts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param post body models.Post true "Post object"
|
||||||
|
// @Success 201 {object} models.Post
|
||||||
|
// @Router /api/v1/admin/posts [post]
|
||||||
|
func (h *PostHandler) CreatePost(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
Title string `json:"title" binding:"required"`
|
||||||
|
UserID *uint64 `json:"user_id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Keywords string `json:"keywords"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Thumb string `json:"thumb"`
|
||||||
|
Video string `json:"video"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
IsActive *bool `json:"is_active"`
|
||||||
|
IsFront *bool `json:"is_front"`
|
||||||
|
ParentID *uint64 `json:"parent_id"`
|
||||||
|
Categories []uint64 `json:"categories"`
|
||||||
|
Tags []uint64 `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
post := &models.Post{
|
||||||
|
Title: input.Title,
|
||||||
|
UserID: input.UserID,
|
||||||
|
Content: input.Content,
|
||||||
|
Keywords: input.Keywords,
|
||||||
|
Image: input.Image,
|
||||||
|
Thumb: input.Thumb,
|
||||||
|
Video: input.Video,
|
||||||
|
Slug: input.Slug,
|
||||||
|
ParentID: input.ParentID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.IsActive != nil {
|
||||||
|
post.IsActive = *input.IsActive
|
||||||
|
} else {
|
||||||
|
post.IsActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.IsFront != nil {
|
||||||
|
post.IsFront = *input.IsFront
|
||||||
|
} else {
|
||||||
|
post.IsFront = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create post first
|
||||||
|
if err := h.service.CreatePost(post); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add categories and tags if provided
|
||||||
|
if len(input.Categories) > 0 || len(input.Tags) > 0 {
|
||||||
|
updates := make(map[string]interface{})
|
||||||
|
|
||||||
|
if len(input.Categories) > 0 {
|
||||||
|
var categories []*models.Category
|
||||||
|
for _, catID := range input.Categories {
|
||||||
|
categories = append(categories, &models.Category{ID: catID})
|
||||||
|
}
|
||||||
|
updates["categories"] = categories
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input.Tags) > 0 {
|
||||||
|
var tags []*models.Tag
|
||||||
|
for _, tagID := range input.Tags {
|
||||||
|
tags = append(tags, &models.Tag{ID: tagID})
|
||||||
|
}
|
||||||
|
updates["tags"] = tags
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdatePost(post.ID, updates); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch created post with relationships
|
||||||
|
createdPost, _ := h.service.GetPostByID(post.ID)
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"data": createdPost})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePost godoc
|
||||||
|
// @Summary Update a post (Admin)
|
||||||
|
// @Description Update an existing post
|
||||||
|
// @Tags admin,posts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Post ID"
|
||||||
|
// @Param post body models.Post true "Post object"
|
||||||
|
// @Success 200 {object} models.Post
|
||||||
|
// @Router /api/v1/admin/posts/{id} [put]
|
||||||
|
func (h *PostHandler) UpdatePost(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdatePost(id, input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch updated post
|
||||||
|
post, err := h.service.GetPostByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": post})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePost godoc
|
||||||
|
// @Summary Delete a post (Admin)
|
||||||
|
// @Description Delete a post by ID
|
||||||
|
// @Tags admin,posts
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Post ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/posts/{id} [delete]
|
||||||
|
func (h *PostHandler) DeletePost(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.DeletePost(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Post deleted successfully"})
|
||||||
|
}
|
||||||
220
app/blog/handlers/tag_handler.go
Normal file
220
app/blog/handlers/tag_handler.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/blog/services"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TagHandler struct {
|
||||||
|
service *services.TagService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTagHandler(service *services.TagService) *TagHandler {
|
||||||
|
return &TagHandler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllTags godoc
|
||||||
|
// @Summary Get all active tags
|
||||||
|
// @Description Get list of all active tags (public endpoint)
|
||||||
|
// @Tags tags
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} models.Tag
|
||||||
|
// @Router /api/v1/tags [get]
|
||||||
|
func (h *TagHandler) GetAllTags(c *gin.Context) {
|
||||||
|
tags, err := h.service.GetAllTags(true)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": tags})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagBySlug godoc
|
||||||
|
// @Summary Get tag by slug
|
||||||
|
// @Description Get a single tag by its slug (public endpoint)
|
||||||
|
// @Tags tags
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param slug path string true "Tag Slug"
|
||||||
|
// @Success 200 {object} models.Tag
|
||||||
|
// @Router /api/v1/tags/{slug} [get]
|
||||||
|
func (h *TagHandler) GetTagBySlug(c *gin.Context) {
|
||||||
|
slug := c.Param("slug")
|
||||||
|
|
||||||
|
tag, err := h.service.GetTagBySlug(slug)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Tag not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": tag})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminGetAllTags godoc
|
||||||
|
// @Summary Get all tags (Admin)
|
||||||
|
// @Description Get list of all tags including inactive ones
|
||||||
|
// @Tags admin,tags
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {array} models.Tag
|
||||||
|
// @Router /api/v1/admin/tags [get]
|
||||||
|
func (h *TagHandler) AdminGetAllTags(c *gin.Context) {
|
||||||
|
tags, err := h.service.GetAllTags(false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": tags})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagByID godoc
|
||||||
|
// @Summary Get tag by ID (Admin)
|
||||||
|
// @Description Get a single tag by ID
|
||||||
|
// @Tags admin,tags
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Tag ID"
|
||||||
|
// @Success 200 {object} models.Tag
|
||||||
|
// @Router /api/v1/admin/tags/{id} [get]
|
||||||
|
func (h *TagHandler) GetTagByID(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tag, err := h.service.GetTagByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Tag not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": tag})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTag godoc
|
||||||
|
// @Summary Create a new tag (Admin)
|
||||||
|
// @Description Create a new tag
|
||||||
|
// @Tags admin,tags
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param tag body models.Tag true "Tag object"
|
||||||
|
// @Success 201 {object} models.Tag
|
||||||
|
// @Router /api/v1/admin/tags [post]
|
||||||
|
func (h *TagHandler) CreateTag(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
Tag string `json:"tag" binding:"required"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
IsActive *bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := &models.Tag{
|
||||||
|
Tag: input.Tag,
|
||||||
|
Slug: input.Slug,
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.IsActive != nil {
|
||||||
|
tag.IsActive = *input.IsActive
|
||||||
|
} else {
|
||||||
|
tag.IsActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreateTag(tag); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"data": tag})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTag godoc
|
||||||
|
// @Summary Update a tag (Admin)
|
||||||
|
// @Description Update an existing tag
|
||||||
|
// @Tags admin,tags
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Tag ID"
|
||||||
|
// @Param tag body models.Tag true "Tag object"
|
||||||
|
// @Success 200 {object} models.Tag
|
||||||
|
// @Router /api/v1/admin/tags/{id} [put]
|
||||||
|
func (h *TagHandler) UpdateTag(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateTag(id, input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch updated tag
|
||||||
|
tag, err := h.service.GetTagByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": tag})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTag godoc
|
||||||
|
// @Summary Delete a tag (Admin)
|
||||||
|
// @Description Delete a tag by ID
|
||||||
|
// @Tags admin,tags
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Tag ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/tags/{id} [delete]
|
||||||
|
func (h *TagHandler) DeleteTag(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid tag ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.DeleteTag(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Tag deleted successfully"})
|
||||||
|
}
|
||||||
107
app/blog/services/category_service.go
Normal file
107
app/blog/services/category_service.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CategoryService struct{}
|
||||||
|
|
||||||
|
func NewCategoryService() *CategoryService {
|
||||||
|
return &CategoryService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCategories retrieves all categories, optionally filtering by active status
|
||||||
|
func (s *CategoryService) GetAllCategories(activeOnly bool) ([]models.Category, error) {
|
||||||
|
var categories []models.Category
|
||||||
|
query := database.DB.Preload("Parent").Preload("Children")
|
||||||
|
|
||||||
|
if activeOnly {
|
||||||
|
query = query.Where("is_active = ?", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.Order("`order` ASC, created_at DESC").Find(&categories).Error
|
||||||
|
return categories, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoryByID retrieves a category by ID with parent and children relationships
|
||||||
|
func (s *CategoryService) GetCategoryByID(id uint64) (*models.Category, error) {
|
||||||
|
var category models.Category
|
||||||
|
err := database.DB.
|
||||||
|
Preload("Parent").
|
||||||
|
Preload("Children").
|
||||||
|
First(&category, id).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &category, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoryBySlug retrieves a category by slug
|
||||||
|
func (s *CategoryService) GetCategoryBySlug(slug string) (*models.Category, error) {
|
||||||
|
var category models.Category
|
||||||
|
err := database.DB.
|
||||||
|
Preload("Parent").
|
||||||
|
Preload("Children").
|
||||||
|
Where("slug = ?", slug).
|
||||||
|
First(&category).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &category, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCategory creates a new category
|
||||||
|
func (s *CategoryService) CreateCategory(category *models.Category) error {
|
||||||
|
return database.DB.Create(category).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCategory updates an existing category
|
||||||
|
func (s *CategoryService) UpdateCategory(id uint64, updates map[string]interface{}) error {
|
||||||
|
result := database.DB.Model(&models.Category{}).Where("id = ?", id).Updates(updates)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCategory deletes a category by ID
|
||||||
|
func (s *CategoryService) DeleteCategory(id uint64) error {
|
||||||
|
result := database.DB.Delete(&models.Category{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoriesByParent retrieves child categories of a parent
|
||||||
|
func (s *CategoryService) GetCategoriesByParent(parentID uint64, activeOnly bool) ([]models.Category, error) {
|
||||||
|
var categories []models.Category
|
||||||
|
query := database.DB.Where("parent_id = ?", parentID)
|
||||||
|
|
||||||
|
if activeOnly {
|
||||||
|
query = query.Where("is_active = ?", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.Order("`order` ASC, created_at DESC").Find(&categories).Error
|
||||||
|
return categories, err
|
||||||
|
}
|
||||||
67
app/blog/services/category_view_service.go
Normal file
67
app/blog/services/category_view_service.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CategoryViewService struct{}
|
||||||
|
|
||||||
|
func NewCategoryViewService() *CategoryViewService {
|
||||||
|
return &CategoryViewService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackCategoryView records a category view
|
||||||
|
func (s *CategoryViewService) TrackCategoryView(categoryID uint64, ipAddress, userAgent string) error {
|
||||||
|
view := &models.CategoryView{
|
||||||
|
CategoryID: categoryID,
|
||||||
|
IPAddress: ipAddress,
|
||||||
|
UserAgent: userAgent,
|
||||||
|
}
|
||||||
|
return database.DB.Create(view).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCategoryViewCount gets total view count for a category
|
||||||
|
func (s *CategoryViewService) GetCategoryViewCount(categoryID uint64) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := database.DB.Model(&models.CategoryView{}).
|
||||||
|
Where("category_id = ?", categoryID).
|
||||||
|
Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCategoryViews gets all views for a category with pagination
|
||||||
|
func (s *CategoryViewService) GetAllCategoryViews(page, limit int) ([]models.CategoryView, int64, error) {
|
||||||
|
var views []models.CategoryView
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := database.DB.Preload("Category")
|
||||||
|
|
||||||
|
query.Model(&models.CategoryView{}).Count(&total)
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Offset((page - 1) * limit).
|
||||||
|
Limit(limit).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&views).Error
|
||||||
|
|
||||||
|
return views, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetViewsByCategory gets views for a specific category
|
||||||
|
func (s *CategoryViewService) GetViewsByCategory(categoryID uint64, page, limit int) ([]models.CategoryView, int64, error) {
|
||||||
|
var views []models.CategoryView
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := database.DB.Where("category_id = ?", categoryID)
|
||||||
|
|
||||||
|
query.Model(&models.CategoryView{}).Count(&total)
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Offset((page - 1) * limit).
|
||||||
|
Limit(limit).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&views).Error
|
||||||
|
|
||||||
|
return views, total, err
|
||||||
|
}
|
||||||
115
app/blog/services/comment_service.go
Normal file
115
app/blog/services/comment_service.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommentService struct{}
|
||||||
|
|
||||||
|
func NewCommentService() *CommentService {
|
||||||
|
return &CommentService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommentsByPost retrieves comments for a specific post
|
||||||
|
func (s *CommentService) GetCommentsByPost(postID uint64, activeOnly bool) ([]models.Comment, error) {
|
||||||
|
var comments []models.Comment
|
||||||
|
query := database.DB.
|
||||||
|
Where("product_id = ?", postID).
|
||||||
|
Preload("Parent").
|
||||||
|
Preload("Children")
|
||||||
|
|
||||||
|
if activeOnly {
|
||||||
|
query = query.Where("is_active = ?", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.Order("created_at DESC").Find(&comments).Error
|
||||||
|
return comments, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllComments retrieves all comments with pagination
|
||||||
|
func (s *CommentService) GetAllComments(page, limit int, activeOnly bool) ([]models.Comment, int64, error) {
|
||||||
|
var comments []models.Comment
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := database.DB.
|
||||||
|
Preload("Product").
|
||||||
|
Preload("Parent").
|
||||||
|
Preload("Children")
|
||||||
|
|
||||||
|
if activeOnly {
|
||||||
|
query = query.Where("is_active = ?", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Model(&models.Comment{}).Count(&total)
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Offset((page - 1) * limit).
|
||||||
|
Limit(limit).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&comments).Error
|
||||||
|
|
||||||
|
return comments, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommentByID retrieves a comment by ID
|
||||||
|
func (s *CommentService) GetCommentByID(id uint64) (*models.Comment, error) {
|
||||||
|
var comment models.Comment
|
||||||
|
err := database.DB.
|
||||||
|
Preload("Product").
|
||||||
|
Preload("Parent").
|
||||||
|
Preload("Children").
|
||||||
|
First(&comment, id).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateComment creates a new comment
|
||||||
|
func (s *CommentService) CreateComment(comment *models.Comment) error {
|
||||||
|
return database.DB.Create(comment).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateComment updates an existing comment
|
||||||
|
func (s *CommentService) UpdateComment(id uint64, updates map[string]interface{}) error {
|
||||||
|
result := database.DB.Model(&models.Comment{}).Where("id = ?", id).Updates(updates)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteComment deletes a comment by ID
|
||||||
|
func (s *CommentService) DeleteComment(id uint64) error {
|
||||||
|
result := database.DB.Delete(&models.Comment{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommentReplies retrieves replies to a specific comment
|
||||||
|
func (s *CommentService) GetCommentReplies(commentID uint64) ([]models.Comment, error) {
|
||||||
|
var replies []models.Comment
|
||||||
|
err := database.DB.
|
||||||
|
Where("parent_id = ? AND is_active = ?", commentID, true).
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&replies).Error
|
||||||
|
|
||||||
|
return replies, err
|
||||||
|
}
|
||||||
202
app/blog/services/post_service.go
Normal file
202
app/blog/services/post_service.go
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostService struct{}
|
||||||
|
|
||||||
|
func NewPostService() *PostService {
|
||||||
|
return &PostService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllPosts retrieves all posts with pagination, optionally filtering by active status
|
||||||
|
func (s *PostService) GetAllPosts(page, limit int, activeOnly bool) ([]models.Post, int64, error) {
|
||||||
|
var posts []models.Post
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := database.DB.
|
||||||
|
Preload("User").
|
||||||
|
Preload("Categories").
|
||||||
|
Preload("Tags").
|
||||||
|
Preload("Parent").
|
||||||
|
Preload("Children")
|
||||||
|
|
||||||
|
if activeOnly {
|
||||||
|
query = query.Where("is_active = ?", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count total
|
||||||
|
query.Model(&models.Post{}).Count(&total)
|
||||||
|
|
||||||
|
// Get paginated results
|
||||||
|
err := query.
|
||||||
|
Offset((page - 1) * limit).
|
||||||
|
Limit(limit).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&posts).Error
|
||||||
|
|
||||||
|
return posts, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPostByID retrieves a post by ID with all relationships
|
||||||
|
func (s *PostService) GetPostByID(id uint64) (*models.Post, error) {
|
||||||
|
var post models.Post
|
||||||
|
err := database.DB.
|
||||||
|
Preload("User").
|
||||||
|
Preload("Categories").
|
||||||
|
Preload("Tags").
|
||||||
|
Preload("Parent").
|
||||||
|
Preload("Children").
|
||||||
|
First(&post, id).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPostBySlug retrieves a post by slug
|
||||||
|
func (s *PostService) GetPostBySlug(slug string) (*models.Post, error) {
|
||||||
|
var post models.Post
|
||||||
|
err := database.DB.
|
||||||
|
Preload("User").
|
||||||
|
Preload("Categories").
|
||||||
|
Preload("Tags").
|
||||||
|
Preload("Parent").
|
||||||
|
Preload("Children").
|
||||||
|
Where("slug = ? AND is_active = ?", slug, true).
|
||||||
|
First(&post).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePost creates a new post
|
||||||
|
func (s *PostService) CreatePost(post *models.Post) error {
|
||||||
|
return database.DB.Create(post).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePost updates an existing post
|
||||||
|
func (s *PostService) UpdatePost(id uint64, updates map[string]interface{}) error {
|
||||||
|
// Handle many-to-many relationships separately if they're in updates
|
||||||
|
var categoryIDs []*models.Category
|
||||||
|
var tagIDs []*models.Tag
|
||||||
|
|
||||||
|
if categories, ok := updates["categories"]; ok {
|
||||||
|
if catSlice, ok := categories.([]*models.Category); ok {
|
||||||
|
categoryIDs = catSlice
|
||||||
|
delete(updates, "categories")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tags, ok := updates["tags"]; ok {
|
||||||
|
if tagSlice, ok := tags.([]*models.Tag); ok {
|
||||||
|
tagIDs = tagSlice
|
||||||
|
delete(updates, "tags")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update basic fields
|
||||||
|
result := database.DB.Model(&models.Post{}).Where("id = ?", id).Updates(updates)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update relationships if provided
|
||||||
|
if len(categoryIDs) > 0 || len(tagIDs) > 0 {
|
||||||
|
var post models.Post
|
||||||
|
if err := database.DB.First(&post, id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(categoryIDs) > 0 {
|
||||||
|
if err := database.DB.Model(&post).Association("Categories").Replace(categoryIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tagIDs) > 0 {
|
||||||
|
if err := database.DB.Model(&post).Association("Tags").Replace(tagIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePost deletes a post by ID
|
||||||
|
func (s *PostService) DeletePost(id uint64) error {
|
||||||
|
result := database.DB.Delete(&models.Post{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPostsByCategory retrieves posts by category ID
|
||||||
|
func (s *PostService) GetPostsByCategory(categoryID uint64, page, limit int) ([]models.Post, int64, error) {
|
||||||
|
var posts []models.Post
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := database.DB.
|
||||||
|
Joins("JOIN post_categories ON post_categories.post_id = posts.id").
|
||||||
|
Where("post_categories.category_id = ? AND posts.is_active = ?", categoryID, true).
|
||||||
|
Preload("User").
|
||||||
|
Preload("Categories").
|
||||||
|
Preload("Tags")
|
||||||
|
|
||||||
|
query.Model(&models.Post{}).Count(&total)
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Offset((page - 1) * limit).
|
||||||
|
Limit(limit).
|
||||||
|
Order("posts.created_at DESC").
|
||||||
|
Find(&posts).Error
|
||||||
|
|
||||||
|
return posts, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPostsByTag retrieves posts by tag ID
|
||||||
|
func (s *PostService) GetPostsByTag(tagID uint64, page, limit int) ([]models.Post, int64, error) {
|
||||||
|
var posts []models.Post
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := database.DB.
|
||||||
|
Joins("JOIN post_tags ON post_tags.post_id = posts.id").
|
||||||
|
Where("post_tags.tag_id = ? AND posts.is_active = ?", tagID, true).
|
||||||
|
Preload("User").
|
||||||
|
Preload("Categories").
|
||||||
|
Preload("Tags")
|
||||||
|
|
||||||
|
query.Model(&models.Post{}).Count(&total)
|
||||||
|
|
||||||
|
err := query.
|
||||||
|
Offset((page - 1) * limit).
|
||||||
|
Limit(limit).
|
||||||
|
Order("posts.created_at DESC").
|
||||||
|
Find(&posts).Error
|
||||||
|
|
||||||
|
return posts, total, err
|
||||||
|
}
|
||||||
87
app/blog/services/tag_service.go
Normal file
87
app/blog/services/tag_service.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TagService struct{}
|
||||||
|
|
||||||
|
func NewTagService() *TagService {
|
||||||
|
return &TagService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllTags retrieves all tags, optionally filtering by active status
|
||||||
|
func (s *TagService) GetAllTags(activeOnly bool) ([]models.Tag, error) {
|
||||||
|
var tags []models.Tag
|
||||||
|
query := database.DB
|
||||||
|
|
||||||
|
if activeOnly {
|
||||||
|
query = query.Where("is_active = ?", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.Order("tag ASC").Find(&tags).Error
|
||||||
|
return tags, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagByID retrieves a tag by ID
|
||||||
|
func (s *TagService) GetTagByID(id uint64) (*models.Tag, error) {
|
||||||
|
var tag models.Tag
|
||||||
|
err := database.DB.First(&tag, id).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagBySlug retrieves a tag by slug
|
||||||
|
func (s *TagService) GetTagBySlug(slug string) (*models.Tag, error) {
|
||||||
|
var tag models.Tag
|
||||||
|
err := database.DB.Where("slug = ?", slug).First(&tag).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTag creates a new tag
|
||||||
|
func (s *TagService) CreateTag(tag *models.Tag) error {
|
||||||
|
return database.DB.Create(tag).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTag updates an existing tag
|
||||||
|
func (s *TagService) UpdateTag(id uint64, updates map[string]interface{}) error {
|
||||||
|
result := database.DB.Model(&models.Tag{}).Where("id = ?", id).Updates(updates)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTag deletes a tag by ID
|
||||||
|
func (s *TagService) DeleteTag(id uint64) error {
|
||||||
|
result := database.DB.Delete(&models.Tag{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
49
app/middlewares/admin_middleware.go
Normal file
49
app/middlewares/admin_middleware.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminMiddleware - Sadece admin rolündeki kullanıcıların erişimini sağlar
|
||||||
|
func AdminMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Get user_id from context (set by AuthMiddleware)
|
||||||
|
userID := c.GetString("user_id")
|
||||||
|
if userID == "" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user with roles
|
||||||
|
var user models.User
|
||||||
|
err := database.DB.Preload("Roles").Where("id = ?", userID).First(&user).Error
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has admin role
|
||||||
|
hasAdminRole := false
|
||||||
|
for _, role := range user.Roles {
|
||||||
|
if role.Name == "admin" {
|
||||||
|
hasAdminRole = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasAdminRole {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/middlewares/auth_middleware.go
Normal file
47
app/middlewares/auth_middleware.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/settings/services"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AuthMiddleware(jwtService *services.JWTService) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
|
||||||
|
claims, err := jwtService.ValidateToken(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("user_id", claims.UserID)
|
||||||
|
c.Set("email", claims.Email)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionalAuthMiddleware checks for a token but doesn't abort if it's missing or invalid.
|
||||||
|
// It sets user_id if a valid token is present.
|
||||||
|
func OptionalAuthMiddleware(jwtService *services.JWTService) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader != "" {
|
||||||
|
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
|
||||||
|
claims, err := jwtService.ValidateToken(tokenString)
|
||||||
|
if err == nil {
|
||||||
|
c.Set("user_id", claims.UserID)
|
||||||
|
c.Set("email", claims.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/middlewares/dynamic_cors_middleware.go
Normal file
57
app/middlewares/dynamic_cors_middleware.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/settings/services"
|
||||||
|
"gobeyhan/config"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DynamicCorsMiddleware - Database'den okunan CORS ayarlarıyla çalışan middleware
|
||||||
|
func DynamicCorsMiddleware(settingsService *services.SettingsService) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
origin := c.Request.Header.Get("Origin")
|
||||||
|
|
||||||
|
// If no origin header, skip CORS
|
||||||
|
if origin == "" {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed, matchedEntry, matchedList, err := settingsService.CheckOrigin(origin)
|
||||||
|
if config.AppConfig != nil && config.AppConfig.CorsDebug {
|
||||||
|
log.Printf("cors_debug origin=%q allowed=%t matched_entry=%q matched_list=%q ip=%q", origin, allowed, matchedEntry, matchedList, c.ClientIP())
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// On error, log and deny
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": "Failed to verify CORS policy",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allowed {
|
||||||
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||||
|
"error": "Origin not allowed by CORS policy",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set CORS headers
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||||
|
c.Writer.Header().Set("Access-Control-Max-Age", "86400") // 24 hours
|
||||||
|
|
||||||
|
// Handle preflight requests
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
200
app/middlewares/rate_limit_middleware.go
Normal file
200
app/middlewares/rate_limit_middleware.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gobeyhan/app/settings/services"
|
||||||
|
"gobeyhan/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RateLimitMiddleware creates a rate limiting middleware
|
||||||
|
func RateLimitMiddleware(maxRequests int64, duration time.Duration) gin.HandlerFunc {
|
||||||
|
cacheService := services.NewCacheService()
|
||||||
|
settingsService := services.NewSettingsService()
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Get client IP
|
||||||
|
clientIP := c.ClientIP()
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
method := c.Request.Method
|
||||||
|
|
||||||
|
// Skip checks for localhost (hardcoded safety)
|
||||||
|
if clientIP == "::1" || clientIP == "127.0.0.1" || clientIP == "localhost" {
|
||||||
|
fmt.Printf("%s[LOCALHOST BYPASS]%s IP: %s accessed %s %s\n", utils.ColorCyan, utils.ColorReset, clientIP, method, path)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Check Blacklist from DB
|
||||||
|
blacklist, err := settingsService.GetActiveBlacklistOrigins()
|
||||||
|
if err == nil {
|
||||||
|
for _, blocked := range blacklist {
|
||||||
|
if blocked == clientIP || strings.Contains(blocked, clientIP) {
|
||||||
|
fmt.Printf("%s[BLACKLIST BLOCKED]%s IP: %s tried to access %s %s\n", utils.ColorRed, utils.ColorReset, clientIP, method, path)
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"error": "Access denied. Your IP is blacklisted.",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check Whitelist from DB (Skip Rate Limit)
|
||||||
|
whitelist, err := settingsService.GetActiveWhitelistOrigins()
|
||||||
|
if err == nil {
|
||||||
|
for _, allowed := range whitelist {
|
||||||
|
if allowed == clientIP || strings.Contains(allowed, clientIP) {
|
||||||
|
fmt.Printf("%s[WHITELIST ALLOWED]%s IP: %s accessed %s %s (Rate Limit Skipped)\n", utils.ColorGreen, utils.ColorReset, clientIP, method, path)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key := clientIP
|
||||||
|
|
||||||
|
// Increment counter
|
||||||
|
count, err := cacheService.IncrementRateLimit(key, duration)
|
||||||
|
if err != nil {
|
||||||
|
// If Redis is down, allow the request but log error
|
||||||
|
fmt.Printf("%s[REDIS ERROR]%s Could not increment rate limit for %s: %v\n", utils.ColorRed, utils.ColorReset, clientIP, err)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := maxRequests - count
|
||||||
|
if remaining < 0 {
|
||||||
|
remaining = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if limit exceeded
|
||||||
|
if count > maxRequests {
|
||||||
|
fmt.Printf("%s[RATE LIMIT EXCEEDED]%s IP: %s - %s %s - Limit: %d\n", utils.ColorYellow, utils.ColorReset, clientIP, method, path, maxRequests)
|
||||||
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
|
"error": "Too many requests. Please try again later.",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log normal access with remaining limit
|
||||||
|
fmt.Printf("[Rate Limit] IP: %s - %s %s - Used: %d/%d - Remaining: %d\n", clientIP, method, path, count, maxRequests, remaining)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DynamicRateLimitMiddleware - Database'den ayarları okuyan rate limit middleware
|
||||||
|
func DynamicRateLimitMiddleware(settingName string, settingsService *services.SettingsService) gin.HandlerFunc {
|
||||||
|
cacheService := services.NewCacheService()
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Get client IP
|
||||||
|
clientIP := c.ClientIP()
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
method := c.Request.Method
|
||||||
|
|
||||||
|
// Skip checks for localhost
|
||||||
|
if clientIP == "::1" || clientIP == "127.0.0.1" || clientIP == "localhost" {
|
||||||
|
fmt.Printf("%s[LOCALHOST BYPASS]%s IP: %s accessed %s %s\n", utils.ColorCyan, utils.ColorReset, clientIP, method, path)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Check Blacklist from DB
|
||||||
|
blacklist, err := settingsService.GetActiveBlacklistOrigins()
|
||||||
|
if err == nil {
|
||||||
|
for _, blocked := range blacklist {
|
||||||
|
if blocked == clientIP || strings.Contains(blocked, clientIP) {
|
||||||
|
fmt.Printf("%s[BLACKLIST BLOCKED]%s IP: %s tried to access %s %s\n", utils.ColorRed, utils.ColorReset, clientIP, method, path)
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"error": "Access denied. Your IP is blacklisted.",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check Whitelist from DB (Skip Rate Limit)
|
||||||
|
whitelist, err := settingsService.GetActiveWhitelistOrigins()
|
||||||
|
if err == nil {
|
||||||
|
for _, allowed := range whitelist {
|
||||||
|
if allowed == clientIP || strings.Contains(allowed, clientIP) {
|
||||||
|
fmt.Printf("%s[WHITELIST ALLOWED]%s IP: %s accessed %s %s (Rate Limit Skipped)\n", utils.ColorGreen, utils.ColorReset, clientIP, method, path)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get rate limit settings from database/cache
|
||||||
|
setting, err := settingsService.GetRateLimitSettingByName(settingName)
|
||||||
|
if err != nil || setting == nil {
|
||||||
|
// If error or not found, use default and allow
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if setting is active
|
||||||
|
if !setting.IsActive {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := settingName + ":" + clientIP
|
||||||
|
|
||||||
|
// Increment counter
|
||||||
|
duration := time.Duration(setting.WindowSeconds) * time.Second
|
||||||
|
count, err := cacheService.IncrementRateLimit(key, duration)
|
||||||
|
if err != nil {
|
||||||
|
// If Redis is down, allow the request
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := setting.MaxRequests - count
|
||||||
|
if remaining < 0 {
|
||||||
|
remaining = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if limit exceeded
|
||||||
|
if count > setting.MaxRequests {
|
||||||
|
fmt.Printf("%s[RATE LIMIT EXCEEDED]%s IP: %s - %s %s - Limit: %d\n", utils.ColorYellow, utils.ColorReset, clientIP, method, path, setting.MaxRequests)
|
||||||
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
|
"error": "Too many requests. Please try again later.",
|
||||||
|
"limit": setting.MaxRequests,
|
||||||
|
"window": setting.WindowSeconds,
|
||||||
|
"retry_after": setting.WindowSeconds,
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log normal access with remaining limit
|
||||||
|
fmt.Printf("[Rate Limit] IP: %s - %s %s - Used: %d/%d - Remaining: %d\n", clientIP, method, path, count, setting.MaxRequests, remaining)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRateLimitMiddleware limits login attempts per IP
|
||||||
|
func LoginRateLimitMiddleware() gin.HandlerFunc {
|
||||||
|
return RateLimitMiddleware(5, 1*time.Minute) // 5 login attempts per minute
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRateLimitMiddleware limits registration attempts per IP
|
||||||
|
func RegisterRateLimitMiddleware() gin.HandlerFunc {
|
||||||
|
return RateLimitMiddleware(3, 5*time.Minute) // 3 registration attempts per 5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIRateLimitMiddleware general API rate limiting
|
||||||
|
func APIRateLimitMiddleware() gin.HandlerFunc {
|
||||||
|
return RateLimitMiddleware(100, 1*time.Minute) // 100 requests per minute
|
||||||
|
}
|
||||||
302
app/routes/routes.go
Normal file
302
app/routes/routes.go
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
accountHandlers "gobeyhan/app/account/handlers"
|
||||||
|
accountServices "gobeyhan/app/account/services"
|
||||||
|
blogHandlers "gobeyhan/app/blog/handlers"
|
||||||
|
blogServices "gobeyhan/app/blog/services"
|
||||||
|
"gobeyhan/app/middlewares"
|
||||||
|
settingsHandlers "gobeyhan/app/settings/handlers"
|
||||||
|
settingsServices "gobeyhan/app/settings/services"
|
||||||
|
adminPkg "gobeyhan/internal/handler/admin"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupRoutes initializes all application routes
|
||||||
|
func SetupRoutes(r *gin.Engine) {
|
||||||
|
// ============================================
|
||||||
|
// BLOG APP - Services & Handlers
|
||||||
|
// ============================================
|
||||||
|
categoryService := blogServices.NewCategoryService()
|
||||||
|
tagService := blogServices.NewTagService()
|
||||||
|
postService := blogServices.NewPostService()
|
||||||
|
commentService := blogServices.NewCommentService()
|
||||||
|
categoryViewService := blogServices.NewCategoryViewService()
|
||||||
|
|
||||||
|
categoryHandler := blogHandlers.NewCategoryHandler(categoryService)
|
||||||
|
tagHandler := blogHandlers.NewTagHandler(tagService)
|
||||||
|
postHandler := blogHandlers.NewPostHandler(postService)
|
||||||
|
commentHandler := blogHandlers.NewCommentHandler(commentService)
|
||||||
|
categoryViewHandler := blogHandlers.NewCategoryViewHandler(categoryViewService)
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ACCOUNT APP - Services & Handlers
|
||||||
|
// ============================================
|
||||||
|
// ============================================
|
||||||
|
// ACCOUNT APP - Services & Handlers
|
||||||
|
// ============================================
|
||||||
|
userService := accountServices.NewUserService()
|
||||||
|
socialAccountService := accountServices.NewSocialAccountService()
|
||||||
|
roleService := accountServices.NewRoleService()
|
||||||
|
permissionService := accountServices.NewPermissionService()
|
||||||
|
|
||||||
|
// Settings & Utils
|
||||||
|
settingsService := settingsServices.NewSettingsService()
|
||||||
|
jwtService := settingsServices.NewJWTService()
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
userHandler := accountHandlers.NewUserHandler(userService)
|
||||||
|
authHandler := accountHandlers.NewAuthHandler(userService, jwtService)
|
||||||
|
oauthHandler := accountHandlers.NewOAuthHandler(userService, socialAccountService, jwtService)
|
||||||
|
// socialAccountHandler := accountHandlers.NewSocialAccountHandler(socialAccountService)
|
||||||
|
roleHandler := accountHandlers.NewRoleHandler(roleService)
|
||||||
|
permissionHandler := accountHandlers.NewPermissionHandler(permissionService)
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SETTINGS APP - Services & Handlers
|
||||||
|
// ============================================
|
||||||
|
settingsHandler := settingsHandlers.NewSettingsHandler(settingsService)
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ADMIN UI ROUTES
|
||||||
|
// ============================================
|
||||||
|
adminHandler := adminPkg.NewHandler()
|
||||||
|
r.GET("/admin/login", adminHandler.LoginPage)
|
||||||
|
r.POST("/admin/login", adminHandler.LoginPost)
|
||||||
|
r.GET("/admin", func(c *gin.Context) {
|
||||||
|
c.Redirect(301, "/admin/dashboard")
|
||||||
|
})
|
||||||
|
r.GET("/admin/dashboard", adminHandler.Dashboard)
|
||||||
|
|
||||||
|
// User CRUD
|
||||||
|
adminUserHandler := adminPkg.NewUserHandler()
|
||||||
|
r.GET("/admin/users", adminUserHandler.List)
|
||||||
|
r.GET("/admin/users/new", adminUserHandler.New)
|
||||||
|
r.POST("/admin/users", adminUserHandler.Create)
|
||||||
|
r.GET("/admin/users/:id/edit", adminUserHandler.Edit)
|
||||||
|
r.POST("/admin/users/:id", adminUserHandler.Update)
|
||||||
|
r.POST("/admin/users/:id/delete", adminUserHandler.Delete)
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// SETTINGS UI ROUTES
|
||||||
|
// ========================================
|
||||||
|
adminSettingsHandler := adminPkg.NewSettingsHandler()
|
||||||
|
|
||||||
|
// Whitelist
|
||||||
|
r.GET("/admin/settings/whitelist", adminSettingsHandler.ListWhitelist)
|
||||||
|
r.GET("/admin/settings/whitelist/new", adminSettingsHandler.NewWhitelist)
|
||||||
|
r.POST("/admin/settings/whitelist", adminSettingsHandler.CreateWhitelist)
|
||||||
|
r.GET("/admin/settings/whitelist/:id/edit", adminSettingsHandler.EditWhitelist)
|
||||||
|
r.POST("/admin/settings/whitelist/:id", adminSettingsHandler.UpdateWhitelist)
|
||||||
|
r.POST("/admin/settings/whitelist/:id/delete", adminSettingsHandler.DeleteWhitelist)
|
||||||
|
|
||||||
|
// Blacklist
|
||||||
|
r.GET("/admin/settings/blacklist", adminSettingsHandler.ListBlacklist)
|
||||||
|
r.GET("/admin/settings/blacklist/new", adminSettingsHandler.NewBlacklist)
|
||||||
|
r.POST("/admin/settings/blacklist", adminSettingsHandler.CreateBlacklist)
|
||||||
|
r.GET("/admin/settings/blacklist/:id/edit", adminSettingsHandler.EditBlacklist)
|
||||||
|
r.POST("/admin/settings/blacklist/:id", adminSettingsHandler.UpdateBlacklist)
|
||||||
|
r.POST("/admin/settings/blacklist/:id/delete", adminSettingsHandler.DeleteBlacklist)
|
||||||
|
|
||||||
|
// Rate Limits
|
||||||
|
r.GET("/admin/settings/rate-limits", adminSettingsHandler.ListRateLimits)
|
||||||
|
r.GET("/admin/settings/rate-limits/:id/edit", adminSettingsHandler.EditRateLimit)
|
||||||
|
r.POST("/admin/settings/rate-limits/:id", adminSettingsHandler.UpdateRateLimit)
|
||||||
|
r.POST("/admin/settings/rate-limits/:id/delete", adminSettingsHandler.DeleteRateLimit)
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// BLOG UI ROUTES
|
||||||
|
// ========================================
|
||||||
|
adminBlogHandler := adminPkg.NewBlogHandler()
|
||||||
|
|
||||||
|
r.GET("/admin/blog", adminBlogHandler.List)
|
||||||
|
r.GET("/admin/blog/new", adminBlogHandler.New)
|
||||||
|
r.POST("/admin/blog", adminBlogHandler.Create)
|
||||||
|
r.GET("/admin/blog/:id/edit", adminBlogHandler.Edit)
|
||||||
|
r.POST("/admin/blog/:id", adminBlogHandler.Update)
|
||||||
|
r.POST("/admin/blog/:id/delete", adminBlogHandler.Delete)
|
||||||
|
|
||||||
|
// Categories
|
||||||
|
r.GET("/admin/blog/categories", adminBlogHandler.ListCategories)
|
||||||
|
r.GET("/admin/blog/categories/new", adminBlogHandler.NewCategory)
|
||||||
|
r.POST("/admin/blog/categories", adminBlogHandler.CreateCategory)
|
||||||
|
r.GET("/admin/blog/categories/:id/edit", adminBlogHandler.EditCategory)
|
||||||
|
r.POST("/admin/blog/categories/:id", adminBlogHandler.UpdateCategory)
|
||||||
|
r.POST("/admin/blog/categories/:id/delete", adminBlogHandler.DeleteCategory)
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
r.GET("/admin/blog/tags", adminBlogHandler.ListTags)
|
||||||
|
r.GET("/admin/blog/tags/new", adminBlogHandler.NewTag)
|
||||||
|
r.POST("/admin/blog/tags", adminBlogHandler.CreateTag)
|
||||||
|
r.GET("/admin/blog/tags/:id/edit", adminBlogHandler.EditTag)
|
||||||
|
r.POST("/admin/blog/tags/:id", adminBlogHandler.UpdateTag)
|
||||||
|
r.POST("/admin/blog/tags/:id/delete", adminBlogHandler.DeleteTag)
|
||||||
|
|
||||||
|
// Comments
|
||||||
|
r.GET("/admin/blog/comments", adminBlogHandler.ListComments)
|
||||||
|
r.GET("/admin/blog/comments/:id/edit", adminBlogHandler.EditComment)
|
||||||
|
r.POST("/admin/blog/comments/:id", adminBlogHandler.UpdateComment)
|
||||||
|
r.POST("/admin/blog/comments/:id/delete", adminBlogHandler.DeleteComment)
|
||||||
|
|
||||||
|
// Static files sharing
|
||||||
|
r.Static("/uploads", "./uploads")
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// API v1 Group
|
||||||
|
// ============================================
|
||||||
|
api := r.Group("/api/v1")
|
||||||
|
api.Use(middlewares.DynamicCorsMiddleware(settingsService))
|
||||||
|
{
|
||||||
|
// ========================================
|
||||||
|
// AUTH ENDPOINTS
|
||||||
|
// ========================================
|
||||||
|
auth := api.Group("/auth")
|
||||||
|
{
|
||||||
|
// Basic Auth
|
||||||
|
auth.POST("/register", authHandler.Register)
|
||||||
|
auth.POST("/login", authHandler.Login)
|
||||||
|
auth.POST("/refresh", authHandler.RefreshToken)
|
||||||
|
auth.POST("/logout", authHandler.Logout)
|
||||||
|
|
||||||
|
// OAuth
|
||||||
|
auth.GET("/google", oauthHandler.GoogleLogin)
|
||||||
|
auth.GET("/google/callback", oauthHandler.GoogleCallback)
|
||||||
|
auth.GET("/github", oauthHandler.GithubLogin)
|
||||||
|
auth.GET("/github/callback", oauthHandler.GithubCallback)
|
||||||
|
|
||||||
|
// Protected
|
||||||
|
auth.GET("/me", middlewares.AuthMiddleware(jwtService), authHandler.GetCurrentUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// PUBLIC ENDPOINTS (Read-only)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// Blog - Categories
|
||||||
|
api.GET("/categories", categoryHandler.GetAllCategories)
|
||||||
|
api.GET("/categories/:slug", categoryHandler.GetCategoryBySlug)
|
||||||
|
api.POST("/categories/:id/view", categoryViewHandler.TrackCategoryView)
|
||||||
|
|
||||||
|
// Blog - Tags
|
||||||
|
api.GET("/tags", tagHandler.GetAllTags)
|
||||||
|
api.GET("/tags/:slug", tagHandler.GetTagBySlug)
|
||||||
|
|
||||||
|
// Blog - Posts
|
||||||
|
api.GET("/posts", postHandler.GetAllPosts)
|
||||||
|
api.GET("/posts/:slug", postHandler.GetPostBySlug)
|
||||||
|
|
||||||
|
// Blog - Comments (separate route to avoid wildcard conflict)
|
||||||
|
api.GET("/comments/post/:postId", commentHandler.GetPostComments)
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// AUTHENTICATED USER ENDPOINTS
|
||||||
|
// ========================================
|
||||||
|
// NOTE: These routes require AuthMiddleware()
|
||||||
|
// Uncomment when authentication middleware is ready
|
||||||
|
|
||||||
|
// user := api.Group("/user")
|
||||||
|
// user.Use(AuthMiddleware())
|
||||||
|
// {
|
||||||
|
// // Blog - Comments (authenticated users can comment)
|
||||||
|
// user.POST("/comments/post/:postId", commentHandler.CreatePostComment)
|
||||||
|
//
|
||||||
|
// // Account - Social Accounts
|
||||||
|
// user.GET("/social-accounts", socialAccountHandler.GetUserSocialAccounts)
|
||||||
|
// user.DELETE("/social-accounts/:id", socialAccountHandler.DeleteSocialAccount)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// ADMIN ENDPOINTS (Protected)
|
||||||
|
// ========================================
|
||||||
|
// NOTE: These routes require AuthMiddleware() + AdminMiddleware()
|
||||||
|
|
||||||
|
admin := api.Group("/admin")
|
||||||
|
admin.Use(middlewares.AuthMiddleware(jwtService), middlewares.AdminMiddleware())
|
||||||
|
{
|
||||||
|
// ========================================
|
||||||
|
// BLOG APP - Admin Routes
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// Categories
|
||||||
|
admin.GET("/categories", categoryHandler.AdminGetAllCategories)
|
||||||
|
admin.GET("/categories/:id", categoryHandler.GetCategoryByID)
|
||||||
|
admin.POST("/categories", categoryHandler.CreateCategory)
|
||||||
|
admin.PUT("/categories/:id", categoryHandler.UpdateCategory)
|
||||||
|
admin.DELETE("/categories/:id", categoryHandler.DeleteCategory)
|
||||||
|
admin.GET("/categories/:id/views", categoryViewHandler.GetCategoryViewStats)
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
admin.GET("/tags", tagHandler.AdminGetAllTags)
|
||||||
|
admin.GET("/tags/:id", tagHandler.GetTagByID)
|
||||||
|
admin.POST("/tags", tagHandler.CreateTag)
|
||||||
|
admin.PUT("/tags/:id", tagHandler.UpdateTag)
|
||||||
|
admin.DELETE("/tags/:id", tagHandler.DeleteTag)
|
||||||
|
|
||||||
|
// Posts
|
||||||
|
admin.GET("/posts", postHandler.AdminGetAllPosts)
|
||||||
|
admin.GET("/posts/:id", postHandler.GetPostByID)
|
||||||
|
admin.POST("/posts", postHandler.CreatePost)
|
||||||
|
admin.PUT("/posts/:id", postHandler.UpdatePost)
|
||||||
|
admin.DELETE("/posts/:id", postHandler.DeletePost)
|
||||||
|
|
||||||
|
// Comments
|
||||||
|
admin.GET("/comments", commentHandler.AdminGetAllComments)
|
||||||
|
admin.GET("/comments/:id", commentHandler.AdminGetCommentByID)
|
||||||
|
admin.PUT("/comments/:id", commentHandler.AdminUpdateComment)
|
||||||
|
admin.DELETE("/comments/:id", commentHandler.AdminDeleteComment)
|
||||||
|
|
||||||
|
// Category Views
|
||||||
|
admin.GET("/category-views", categoryViewHandler.AdminGetAllCategoryViews)
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// ACCOUNT APP - Admin Routes
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// Users
|
||||||
|
admin.GET("/users", userHandler.AdminGetAllUsers)
|
||||||
|
admin.GET("/users/:id", userHandler.AdminGetUserByID)
|
||||||
|
admin.POST("/users", userHandler.AdminCreateUser)
|
||||||
|
admin.PUT("/users/:id", userHandler.AdminUpdateUser)
|
||||||
|
admin.DELETE("/users/:id", userHandler.AdminDeleteUser)
|
||||||
|
admin.POST("/users/:id/restore", userHandler.AdminRestoreUser)
|
||||||
|
admin.POST("/users/:id/roles", userHandler.AdminAssignRole)
|
||||||
|
admin.DELETE("/users/:id/roles/:role_id", userHandler.AdminRemoveRole)
|
||||||
|
|
||||||
|
// Roles
|
||||||
|
admin.GET("/roles", roleHandler.AdminGetAllRoles)
|
||||||
|
admin.GET("/roles/:id", roleHandler.AdminGetRoleByID)
|
||||||
|
admin.POST("/roles", roleHandler.AdminCreateRole)
|
||||||
|
admin.PUT("/roles/:id", roleHandler.AdminUpdateRole)
|
||||||
|
admin.DELETE("/roles/:id", roleHandler.AdminDeleteRole)
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
admin.GET("/permissions", permissionHandler.AdminGetAllPermissions)
|
||||||
|
admin.POST("/permissions", permissionHandler.AdminCreatePermission)
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// SETTINGS APP - Admin Routes
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// CORS Whitelist
|
||||||
|
admin.GET("/cors/whitelist", settingsHandler.GetAllWhitelist)
|
||||||
|
admin.POST("/cors/whitelist", settingsHandler.CreateWhitelist)
|
||||||
|
admin.PUT("/cors/whitelist/:id", settingsHandler.UpdateWhitelist)
|
||||||
|
admin.DELETE("/cors/whitelist/:id", settingsHandler.DeleteWhitelist)
|
||||||
|
|
||||||
|
// CORS Blacklist
|
||||||
|
admin.GET("/cors/blacklist", settingsHandler.GetAllBlacklist)
|
||||||
|
admin.POST("/cors/blacklist", settingsHandler.CreateBlacklist)
|
||||||
|
admin.PUT("/cors/blacklist/:id", settingsHandler.UpdateBlacklist)
|
||||||
|
admin.DELETE("/cors/blacklist/:id", settingsHandler.DeleteBlacklist)
|
||||||
|
|
||||||
|
// CORS Cache
|
||||||
|
admin.POST("/cors/cache/invalidate", settingsHandler.InvalidateCorsCache)
|
||||||
|
|
||||||
|
// Rate Limits
|
||||||
|
admin.GET("/rate-limits", settingsHandler.GetAllRateLimits)
|
||||||
|
admin.PUT("/rate-limits/:id", settingsHandler.UpdateRateLimit)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
462
app/routes/routes.go.backup
Normal file
462
app/routes/routes.go.backup
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/middlewares"
|
||||||
|
"gobeyhan/app/services"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes(r *gin.Engine) {
|
||||||
|
jwtService := services.NewJWTService()
|
||||||
|
authService := services.NewAuthService()
|
||||||
|
authHandler := handlers.NewAuthHandler(authService)
|
||||||
|
|
||||||
|
settingsService := services.NewSettingsService()
|
||||||
|
settingsHandler := handlers.NewSettingsHandler(settingsService)
|
||||||
|
|
||||||
|
userManagementService := services.NewUserManagementService()
|
||||||
|
userManagementHandler := handlers.NewUserManagementHandler(userManagementService)
|
||||||
|
|
||||||
|
avatarHandler := handlers.NewAvatarHandler()
|
||||||
|
profileHandler := handlers.NewProfileHandler()
|
||||||
|
|
||||||
|
contactService := services.NewContactService()
|
||||||
|
contactHandler := handlers.NewContactHandler(contactService)
|
||||||
|
|
||||||
|
tagService := services.NewTagService()
|
||||||
|
tagHandler := handlers.NewTagHandler(tagService)
|
||||||
|
|
||||||
|
postCategoryService := services.NewPostCategoryService()
|
||||||
|
postCategoryHandler := handlers.NewPostCategoryHandler(postCategoryService)
|
||||||
|
|
||||||
|
postTagService := services.NewPostTagService()
|
||||||
|
postTagHandler := handlers.NewPostTagHandler(postTagService)
|
||||||
|
|
||||||
|
postService := services.NewPostService()
|
||||||
|
postHandler := handlers.NewPostHandler(postService)
|
||||||
|
|
||||||
|
postCommentService := services.NewPostCommentService()
|
||||||
|
postCommentHandler := handlers.NewPostCommentHandler(postCommentService)
|
||||||
|
|
||||||
|
postCategoryViewService := services.NewPostCategoryViewService()
|
||||||
|
postCategoryViewHandler := handlers.NewPostCategoryViewHandler(postCategoryViewService)
|
||||||
|
|
||||||
|
homeService := services.NewHomeService()
|
||||||
|
homeHandler := handlers.NewHomeHandler(homeService)
|
||||||
|
|
||||||
|
aboutService := services.NewAboutService()
|
||||||
|
aboutHandler := handlers.NewAboutHandler(aboutService)
|
||||||
|
|
||||||
|
serviceService := services.NewServiceService()
|
||||||
|
serviceHandler := handlers.NewServiceHandler(serviceService)
|
||||||
|
|
||||||
|
serviceTitleService := services.NewServiceTitleService()
|
||||||
|
serviceTitleHandler := handlers.NewServiceTitleHandler(serviceTitleService)
|
||||||
|
|
||||||
|
siteInfoService := services.NewSiteInfoService()
|
||||||
|
siteInfoHandler := handlers.NewSiteInfoHandler(siteInfoService)
|
||||||
|
|
||||||
|
bannerService := services.NewBannerService()
|
||||||
|
bannerHandler := handlers.NewBannerHandler(bannerService)
|
||||||
|
|
||||||
|
siteSettingsService := services.NewSiteSettingsService()
|
||||||
|
siteSettingsHandler := handlers.NewSiteSettingsHandler(siteSettingsService)
|
||||||
|
|
||||||
|
resumeService := services.NewResumeService()
|
||||||
|
resumeHandler := handlers.NewResumeHandler(resumeService)
|
||||||
|
|
||||||
|
educationService := services.NewEducationService()
|
||||||
|
educationHandler := handlers.NewEducationHandler(educationService)
|
||||||
|
|
||||||
|
experienceService := services.NewExperienceService()
|
||||||
|
experienceHandler := handlers.NewExperienceHandler(experienceService)
|
||||||
|
|
||||||
|
skillService := services.NewSkillService()
|
||||||
|
skillHandler := handlers.NewSkillHandler(skillService)
|
||||||
|
|
||||||
|
knowledgeService := services.NewKnowledgeService()
|
||||||
|
knowledgeHandler := handlers.NewKnowledgeHandler(knowledgeService)
|
||||||
|
|
||||||
|
mainMenuService := services.NewMainMenuService()
|
||||||
|
mainMenuHandler := handlers.NewMainMenuHandler(mainMenuService)
|
||||||
|
|
||||||
|
// Serve static files (uploaded avatars)
|
||||||
|
r.Static("/uploads", "./uploads")
|
||||||
|
|
||||||
|
// Homepage
|
||||||
|
r.LoadHTMLGlob("web/*")
|
||||||
|
r.GET("/", func(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "index.html", nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Swagger route moved outside of v1 group to be accessible at /docs/index.html
|
||||||
|
r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
|
||||||
|
v1 := r.Group("/api/v1")
|
||||||
|
v1.Use(middlewares.APIRateLimitMiddleware()) // General API rate limiting
|
||||||
|
{
|
||||||
|
auth := v1.Group("/auth")
|
||||||
|
{
|
||||||
|
auth.POST("/register", middlewares.RegisterRateLimitMiddleware(), authHandler.Register)
|
||||||
|
auth.POST("/login", middlewares.LoginRateLimitMiddleware(), authHandler.Login)
|
||||||
|
auth.GET("/verify-email", authHandler.VerifyEmail)
|
||||||
|
auth.GET("/:provider", authHandler.BeginAuth)
|
||||||
|
auth.GET("/:provider/callback", authHandler.Callback)
|
||||||
|
auth.POST("/refresh", authHandler.Refresh)
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
protected := auth.Group("/")
|
||||||
|
protected.Use(middlewares.AuthMiddleware(jwtService))
|
||||||
|
{
|
||||||
|
protected.GET("/me", authHandler.Me)
|
||||||
|
protected.GET("/validate", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Token is valid",
|
||||||
|
"user_id": c.GetString("user_id"),
|
||||||
|
"email": c.GetString("email"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contact endpoint (Public but can optionally use auth)
|
||||||
|
v1.POST("/contact", middlewares.OptionalAuthMiddleware(jwtService), contactHandler.CreateContact)
|
||||||
|
|
||||||
|
// Public Tags Endpoint (Only active tags)
|
||||||
|
v1.GET("/tags", tagHandler.GetAllTags)
|
||||||
|
|
||||||
|
// Public Post Categories
|
||||||
|
v1.GET("/post-categories", postCategoryHandler.GetAllPostCategories)
|
||||||
|
v1.GET("/post-categories/:slug", postCategoryHandler.GetPostCategoryBySlug)
|
||||||
|
v1.POST("/post-categories/:id/views", postCategoryViewHandler.TrackPostCategoryView)
|
||||||
|
|
||||||
|
// Public Post Tags
|
||||||
|
v1.GET("/post-tags", postTagHandler.GetAllPostTags)
|
||||||
|
|
||||||
|
// Public Posts
|
||||||
|
v1.GET("/posts", postHandler.GetAllPosts)
|
||||||
|
v1.GET("/posts/slug/:slug", postHandler.GetPostBySlug)
|
||||||
|
v1.GET("/posts/:id/comments", postCommentHandler.GetPostCommentsByPostID)
|
||||||
|
|
||||||
|
// Public About Endpoints (Only active about entries)
|
||||||
|
v1.GET("/about", aboutHandler.GetAllAbout)
|
||||||
|
v1.GET("/about/active", aboutHandler.GetActiveAbout)
|
||||||
|
|
||||||
|
// Public Services Endpoints (Only active services)
|
||||||
|
v1.GET("/services", serviceHandler.GetAllServices)
|
||||||
|
v1.GET("/services/:slug", serviceHandler.GetServiceBySlug)
|
||||||
|
|
||||||
|
// Public Service Titles Endpoints (Only active service titles)
|
||||||
|
v1.GET("/service-titles", serviceTitleHandler.GetAllServiceTitles)
|
||||||
|
v1.GET("/service-titles/active", serviceTitleHandler.GetActiveServiceTitle)
|
||||||
|
|
||||||
|
// Public Main Menu Endpoints (Only active entries)
|
||||||
|
v1.GET("/main-menu", mainMenuHandler.GetAllMainMenus)
|
||||||
|
v1.GET("/main-menu/active", mainMenuHandler.GetActiveMainMenu)
|
||||||
|
|
||||||
|
// Public Site Info Endpoints (Only active entries)
|
||||||
|
v1.GET("/site-info", siteInfoHandler.GetAllSiteInfos)
|
||||||
|
v1.GET("/site-info/active", siteInfoHandler.GetActiveSiteInfo)
|
||||||
|
|
||||||
|
// Public Banner Endpoints (Only active entries)
|
||||||
|
v1.GET("/banners", bannerHandler.GetAllBanners)
|
||||||
|
v1.GET("/banners/active", bannerHandler.GetActiveBanner)
|
||||||
|
|
||||||
|
// Public Site Settings Endpoints (Only active entries)
|
||||||
|
v1.GET("/site-settings", siteSettingsHandler.GetAllSiteSettings)
|
||||||
|
v1.GET("/site-settings/active", siteSettingsHandler.GetActiveSiteSettings)
|
||||||
|
|
||||||
|
// Public Homes Endpoints (Only active homes)
|
||||||
|
v1.GET("/homes", homeHandler.GetAllHomes)
|
||||||
|
v1.GET("/homes/:slug", homeHandler.GetHomeBySlug)
|
||||||
|
|
||||||
|
// Public Resume Endpoints
|
||||||
|
v1.GET("/resumes", resumeHandler.GetAllResumes)
|
||||||
|
v1.GET("/resumes/active", resumeHandler.GetActiveResume)
|
||||||
|
v1.GET("/educations", educationHandler.GetAllEducations)
|
||||||
|
v1.GET("/experiences", experienceHandler.GetAllExperiences)
|
||||||
|
v1.GET("/skills", skillHandler.GetAllSkills)
|
||||||
|
v1.GET("/knowledges", knowledgeHandler.GetAllKnowledges)
|
||||||
|
|
||||||
|
// User endpoints
|
||||||
|
user := v1.Group("/user")
|
||||||
|
user.Use(middlewares.AuthMiddleware(jwtService))
|
||||||
|
{
|
||||||
|
// Avatar management
|
||||||
|
user.POST("/avatar", avatarHandler.UploadAvatar)
|
||||||
|
user.DELETE("/avatar", avatarHandler.DeleteAvatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post comment creation (Auth required)
|
||||||
|
postAuth := v1.Group("/posts")
|
||||||
|
postAuth.Use(middlewares.AuthMiddleware(jwtService))
|
||||||
|
{
|
||||||
|
postAuth.POST("/:id/comments", postCommentHandler.CreatePostComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile endpoints
|
||||||
|
profile := v1.Group("/profile")
|
||||||
|
profile.Use(middlewares.AuthMiddleware(jwtService))
|
||||||
|
{
|
||||||
|
profile.GET("", profileHandler.GetProfile)
|
||||||
|
profile.PUT("", profileHandler.UpdateProfile)
|
||||||
|
profile.PUT("/password", profileHandler.ChangePassword)
|
||||||
|
profile.PUT("/email", profileHandler.ChangeEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings endpoints (Admin only)
|
||||||
|
settings := v1.Group("/settings")
|
||||||
|
settings.Use(middlewares.AuthMiddleware(jwtService))
|
||||||
|
settings.Use(middlewares.AdminMiddleware())
|
||||||
|
{
|
||||||
|
// CORS Whitelist
|
||||||
|
corsWhitelist := settings.Group("/cors/whitelist")
|
||||||
|
{
|
||||||
|
corsWhitelist.GET("", settingsHandler.GetAllWhitelist)
|
||||||
|
corsWhitelist.POST("", settingsHandler.CreateWhitelist)
|
||||||
|
corsWhitelist.PUT("/:id", settingsHandler.UpdateWhitelist)
|
||||||
|
corsWhitelist.DELETE("/:id", settingsHandler.DeleteWhitelist)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS Blacklist
|
||||||
|
corsBlacklist := settings.Group("/cors/blacklist")
|
||||||
|
{
|
||||||
|
corsBlacklist.GET("", settingsHandler.GetAllBlacklist)
|
||||||
|
corsBlacklist.POST("", settingsHandler.CreateBlacklist)
|
||||||
|
corsBlacklist.PUT("/:id", settingsHandler.UpdateBlacklist)
|
||||||
|
corsBlacklist.DELETE("/:id", settingsHandler.DeleteBlacklist)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate Limit Settings
|
||||||
|
rateLimit := settings.Group("/ratelimit")
|
||||||
|
{
|
||||||
|
rateLimit.GET("", settingsHandler.GetAllRateLimits)
|
||||||
|
rateLimit.PUT("/:id", settingsHandler.UpdateRateLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS Cache
|
||||||
|
settings.POST("/cors/cache/invalidate", settingsHandler.InvalidateCorsCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - User Management
|
||||||
|
admin := v1.Group("/admin")
|
||||||
|
admin.Use(middlewares.AuthMiddleware(jwtService))
|
||||||
|
admin.Use(middlewares.AdminMiddleware())
|
||||||
|
{
|
||||||
|
users := admin.Group("/users")
|
||||||
|
{
|
||||||
|
users.GET("/search", userManagementHandler.SearchUsers)
|
||||||
|
users.GET("/deleted", userManagementHandler.GetDeletedUsers) // Yeni: Silinen kullanıcılar
|
||||||
|
users.GET("", userManagementHandler.GetAllUsers)
|
||||||
|
users.POST("", userManagementHandler.CreateUser)
|
||||||
|
users.GET("/:id", userManagementHandler.GetUserByID)
|
||||||
|
users.PUT("/:id", userManagementHandler.UpdateUser)
|
||||||
|
users.DELETE("/:id", userManagementHandler.DeleteUser)
|
||||||
|
users.POST("/:id/roles", userManagementHandler.AssignRoles)
|
||||||
|
users.DELETE("/:id/roles/:role", userManagementHandler.RemoveRole)
|
||||||
|
users.POST("/:id/restore", userManagementHandler.RestoreUser) // Yeni: Kullanıcıyı restore et
|
||||||
|
|
||||||
|
// Avatar management for users (Admin)
|
||||||
|
users.POST("/:id/avatar", avatarHandler.AdminUploadAvatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Home Management
|
||||||
|
homes := admin.Group("/homes")
|
||||||
|
{
|
||||||
|
homes.GET("", homeHandler.AdminGetAllHomes)
|
||||||
|
homes.POST("", homeHandler.CreateHome)
|
||||||
|
homes.GET("/:id", homeHandler.AdminGetHomeByID)
|
||||||
|
homes.PUT("/:id", homeHandler.UpdateHome)
|
||||||
|
homes.DELETE("/:id", homeHandler.DeleteHome)
|
||||||
|
homes.POST("/:id/image", homeHandler.AdminUploadHomeImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Post Categories
|
||||||
|
postCategories := admin.Group("/post-categories")
|
||||||
|
{
|
||||||
|
postCategories.GET("", postCategoryHandler.AdminGetAllPostCategories)
|
||||||
|
postCategories.POST("", postCategoryHandler.CreatePostCategory)
|
||||||
|
postCategories.GET("/:id", postCategoryHandler.AdminGetPostCategoryByID)
|
||||||
|
postCategories.PUT("/:id", postCategoryHandler.UpdatePostCategory)
|
||||||
|
postCategories.DELETE("/:id", postCategoryHandler.DeletePostCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Post Tags
|
||||||
|
postTags := admin.Group("/post-tags")
|
||||||
|
{
|
||||||
|
postTags.GET("", postTagHandler.AdminGetAllPostTags)
|
||||||
|
postTags.POST("", postTagHandler.CreatePostTag)
|
||||||
|
postTags.GET("/:id", postTagHandler.GetPostTagByID)
|
||||||
|
postTags.PUT("/:id", postTagHandler.UpdatePostTag)
|
||||||
|
postTags.DELETE("/:id", postTagHandler.DeletePostTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Posts
|
||||||
|
posts := admin.Group("/posts")
|
||||||
|
{
|
||||||
|
posts.GET("", postHandler.AdminGetAllPosts)
|
||||||
|
posts.POST("", postHandler.CreatePost)
|
||||||
|
posts.GET("/:id", postHandler.AdminGetPostByID)
|
||||||
|
posts.PUT("/:id", postHandler.UpdatePost)
|
||||||
|
posts.DELETE("/:id", postHandler.DeletePost)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Post Comments
|
||||||
|
postComments := admin.Group("/post-comments")
|
||||||
|
{
|
||||||
|
postComments.GET("", postCommentHandler.AdminGetAllPostComments)
|
||||||
|
postComments.GET("/:id", postCommentHandler.AdminGetPostCommentByID)
|
||||||
|
postComments.PUT("/:id", postCommentHandler.AdminUpdatePostComment)
|
||||||
|
postComments.DELETE("/:id", postCommentHandler.AdminDeletePostComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Post Category Views
|
||||||
|
postCategoryViews := admin.Group("/post-category-views")
|
||||||
|
{
|
||||||
|
postCategoryViews.GET("", postCategoryViewHandler.AdminGetPostCategoryViews)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - About Management
|
||||||
|
about := admin.Group("/about")
|
||||||
|
{
|
||||||
|
about.GET("", aboutHandler.AdminGetAllAbout)
|
||||||
|
about.POST("", aboutHandler.CreateAbout)
|
||||||
|
about.GET("/:id", aboutHandler.AdminGetAboutByID)
|
||||||
|
about.PUT("/:id", aboutHandler.UpdateAbout)
|
||||||
|
about.DELETE("/:id", aboutHandler.DeleteAbout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Service Management
|
||||||
|
servicesGroup := admin.Group("/services")
|
||||||
|
{
|
||||||
|
servicesGroup.GET("", serviceHandler.AdminGetAllServices)
|
||||||
|
servicesGroup.POST("", serviceHandler.CreateService)
|
||||||
|
servicesGroup.GET("/:id", serviceHandler.AdminGetServiceByID)
|
||||||
|
servicesGroup.PUT("/:id", serviceHandler.UpdateService)
|
||||||
|
servicesGroup.DELETE("/:id", serviceHandler.DeleteService)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Service Title Management
|
||||||
|
serviceTitles := admin.Group("/service-titles")
|
||||||
|
{
|
||||||
|
serviceTitles.GET("", serviceTitleHandler.AdminGetAllServiceTitles)
|
||||||
|
serviceTitles.POST("", serviceTitleHandler.CreateServiceTitle)
|
||||||
|
serviceTitles.GET("/:id", serviceTitleHandler.AdminGetServiceTitleByID)
|
||||||
|
serviceTitles.PUT("/:id", serviceTitleHandler.UpdateServiceTitle)
|
||||||
|
serviceTitles.DELETE("/:id", serviceTitleHandler.DeleteServiceTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Site Info Management
|
||||||
|
siteInfo := admin.Group("/site-info")
|
||||||
|
{
|
||||||
|
siteInfo.GET("", siteInfoHandler.AdminGetAllSiteInfos)
|
||||||
|
siteInfo.POST("", siteInfoHandler.CreateSiteInfo)
|
||||||
|
siteInfo.GET("/:id", siteInfoHandler.AdminGetSiteInfoByID)
|
||||||
|
siteInfo.PUT("/:id", siteInfoHandler.UpdateSiteInfo)
|
||||||
|
siteInfo.DELETE("/:id", siteInfoHandler.DeleteSiteInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Banner Management
|
||||||
|
banners := admin.Group("/banners")
|
||||||
|
{
|
||||||
|
banners.GET("", bannerHandler.AdminGetAllBanners)
|
||||||
|
banners.POST("", bannerHandler.CreateBanner)
|
||||||
|
banners.GET("/:id", bannerHandler.AdminGetBannerByID)
|
||||||
|
banners.PUT("/:id", bannerHandler.UpdateBanner)
|
||||||
|
banners.DELETE("/:id", bannerHandler.DeleteBanner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Site Settings Management
|
||||||
|
siteSettings := admin.Group("/site-settings")
|
||||||
|
{
|
||||||
|
siteSettings.GET("", siteSettingsHandler.AdminGetAllSiteSettings)
|
||||||
|
siteSettings.POST("", siteSettingsHandler.CreateSiteSettings)
|
||||||
|
siteSettings.GET("/:id", siteSettingsHandler.AdminGetSiteSettingsByID)
|
||||||
|
siteSettings.PUT("/:id", siteSettingsHandler.UpdateSiteSettings)
|
||||||
|
siteSettings.DELETE("/:id", siteSettingsHandler.DeleteSiteSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Resume Management
|
||||||
|
resumes := admin.Group("/resumes")
|
||||||
|
{
|
||||||
|
resumes.GET("", resumeHandler.AdminGetAllResumes)
|
||||||
|
resumes.POST("", resumeHandler.CreateResume)
|
||||||
|
resumes.GET("/:id", resumeHandler.AdminGetResumeByID)
|
||||||
|
resumes.PUT("/:id", resumeHandler.UpdateResume)
|
||||||
|
resumes.DELETE("/:id", resumeHandler.DeleteResume)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Education Management
|
||||||
|
educations := admin.Group("/educations")
|
||||||
|
{
|
||||||
|
educations.GET("", educationHandler.AdminGetAllEducations)
|
||||||
|
educations.POST("", educationHandler.CreateEducation)
|
||||||
|
educations.GET("/:id", educationHandler.AdminGetEducationByID)
|
||||||
|
educations.PUT("/:id", educationHandler.UpdateEducation)
|
||||||
|
educations.DELETE("/:id", educationHandler.DeleteEducation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Experience Management
|
||||||
|
experiences := admin.Group("/experiences")
|
||||||
|
{
|
||||||
|
experiences.GET("", experienceHandler.AdminGetAllExperiences)
|
||||||
|
experiences.POST("", experienceHandler.CreateExperience)
|
||||||
|
experiences.GET("/:id", experienceHandler.AdminGetExperienceByID)
|
||||||
|
experiences.PUT("/:id", experienceHandler.UpdateExperience)
|
||||||
|
experiences.DELETE("/:id", experienceHandler.DeleteExperience)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Skill Management
|
||||||
|
skills := admin.Group("/skills")
|
||||||
|
{
|
||||||
|
skills.GET("", skillHandler.AdminGetAllSkills)
|
||||||
|
skills.POST("", skillHandler.CreateSkill)
|
||||||
|
skills.GET("/:id", skillHandler.AdminGetSkillByID)
|
||||||
|
skills.PUT("/:id", skillHandler.UpdateSkill)
|
||||||
|
skills.DELETE("/:id", skillHandler.DeleteSkill)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Knowledge Management
|
||||||
|
knowledges := admin.Group("/knowledges")
|
||||||
|
{
|
||||||
|
knowledges.GET("", knowledgeHandler.AdminGetAllKnowledges)
|
||||||
|
knowledges.POST("", knowledgeHandler.CreateKnowledge)
|
||||||
|
knowledges.GET("/:id", knowledgeHandler.AdminGetKnowledgeByID)
|
||||||
|
knowledges.PUT("/:id", knowledgeHandler.UpdateKnowledge)
|
||||||
|
knowledges.DELETE("/:id", knowledgeHandler.DeleteKnowledge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Main Menu Management
|
||||||
|
mainMenu := admin.Group("/main-menu")
|
||||||
|
{
|
||||||
|
mainMenu.GET("", mainMenuHandler.AdminGetAllMainMenus)
|
||||||
|
mainMenu.POST("", mainMenuHandler.CreateMainMenu)
|
||||||
|
mainMenu.GET("/:id", mainMenuHandler.AdminGetMainMenuByID)
|
||||||
|
mainMenu.PUT("/:id", mainMenuHandler.UpdateMainMenu)
|
||||||
|
mainMenu.DELETE("/:id", mainMenuHandler.DeleteMainMenu)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Contact Management
|
||||||
|
contacts := admin.Group("/contacts")
|
||||||
|
{
|
||||||
|
contacts.GET("", contactHandler.GetAllContacts)
|
||||||
|
contacts.GET("/:id", contactHandler.GetContactByID)
|
||||||
|
contacts.DELETE("/:id", contactHandler.DeleteContact)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin - Tag Management
|
||||||
|
tags := admin.Group("/tags")
|
||||||
|
{
|
||||||
|
tags.GET("", tagHandler.AdminGetAllTags)
|
||||||
|
tags.POST("", tagHandler.CreateTag)
|
||||||
|
tags.GET("/:id", tagHandler.GetTagByID)
|
||||||
|
tags.PUT("/:id", tagHandler.UpdateTag)
|
||||||
|
tags.DELETE("/:id", tagHandler.DeleteTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
264
app/settings/handlers/settings_handler.go
Normal file
264
app/settings/handlers/settings_handler.go
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/settings/services"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SettingsHandler struct {
|
||||||
|
service *services.SettingsService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettingsHandler(service *services.SettingsService) *SettingsHandler {
|
||||||
|
return &SettingsHandler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllWhitelist godoc
|
||||||
|
// @Summary Get all CORS whitelist entries (Admin)
|
||||||
|
// @Description Get all CORS whitelist origins
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {array} models.CorsWhitelist
|
||||||
|
// @Router /api/v1/admin/cors/whitelist [get]
|
||||||
|
func (h *SettingsHandler) GetAllWhitelist(c *gin.Context) {
|
||||||
|
whitelist, err := h.service.GetAllCorsWhitelist()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": whitelist})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateWhitelist godoc
|
||||||
|
// @Summary Create CORS whitelist entry (Admin)
|
||||||
|
// @Description Add a new origin to CORS whitelist
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param whitelist body models.CorsWhitelist true "Whitelist object"
|
||||||
|
// @Success 201 {object} models.CorsWhitelist
|
||||||
|
// @Router /api/v1/admin/cors/whitelist [post]
|
||||||
|
func (h *SettingsHandler) CreateWhitelist(c *gin.Context) {
|
||||||
|
var input models.CorsWhitelist
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreateCorsWhitelist(&input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"data": input})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWhitelist godoc
|
||||||
|
// @Summary Update CORS whitelist entry (Admin)
|
||||||
|
// @Description Update an existing CORS whitelist entry
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Whitelist ID"
|
||||||
|
// @Param whitelist body models.CorsWhitelist true "Whitelist object"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/cors/whitelist/{id} [put]
|
||||||
|
func (h *SettingsHandler) UpdateWhitelist(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
var input map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateCorsWhitelist(id, input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Whitelist updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWhitelist godoc
|
||||||
|
// @Summary Delete CORS whitelist entry (Admin)
|
||||||
|
// @Description Delete a CORS whitelist entry
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Whitelist ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/cors/whitelist/{id} [delete]
|
||||||
|
func (h *SettingsHandler) DeleteWhitelist(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
if err := h.service.DeleteCorsWhitelist(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Whitelist deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllBlacklist godoc
|
||||||
|
// @Summary Get all CORS blacklist entries (Admin)
|
||||||
|
// @Description Get all CORS blacklist origins
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {array} models.CorsBlacklist
|
||||||
|
// @Router /api/v1/admin/cors/blacklist [get]
|
||||||
|
func (h *SettingsHandler) GetAllBlacklist(c *gin.Context) {
|
||||||
|
blacklist, err := h.service.GetAllCorsBlacklist()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": blacklist})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBlacklist godoc
|
||||||
|
// @Summary Create CORS blacklist entry (Admin)
|
||||||
|
// @Description Add a new origin to CORS blacklist
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param blacklist body models.CorsBlacklist true "Blacklist object"
|
||||||
|
// @Success 201 {object} models.CorsBlacklist
|
||||||
|
// @Router /api/v1/admin/cors/blacklist [post]
|
||||||
|
func (h *SettingsHandler) CreateBlacklist(c *gin.Context) {
|
||||||
|
var input models.CorsBlacklist
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreateCorsBlacklist(&input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"data": input})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBlacklist godoc
|
||||||
|
// @Summary Update CORS blacklist entry (Admin)
|
||||||
|
// @Description Update an existing CORS blacklist entry
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Blacklist ID"
|
||||||
|
// @Param blacklist body models.CorsBlacklist true "Blacklist object"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/cors/blacklist/{id} [put]
|
||||||
|
func (h *SettingsHandler) UpdateBlacklist(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
var input map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateCorsBlacklist(id, input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Blacklist updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBlacklist godoc
|
||||||
|
// @Summary Delete CORS blacklist entry (Admin)
|
||||||
|
// @Description Delete a CORS blacklist entry
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Blacklist ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/cors/blacklist/{id} [delete]
|
||||||
|
func (h *SettingsHandler) DeleteBlacklist(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
if err := h.service.DeleteCorsBlacklist(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Blacklist deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllRateLimits godoc
|
||||||
|
// @Summary Get all rate limit settings (Admin)
|
||||||
|
// @Description Get all rate limit configurations
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {array} models.RateLimitSetting
|
||||||
|
// @Router /api/v1/admin/rate-limits [get]
|
||||||
|
func (h *SettingsHandler) GetAllRateLimits(c *gin.Context) {
|
||||||
|
settings, err := h.service.GetAllRateLimitSettings()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": settings})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRateLimit godoc
|
||||||
|
// @Summary Update rate limit setting (Admin)
|
||||||
|
// @Description Update an existing rate limit configuration
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param id path int true "Rate Limit ID"
|
||||||
|
// @Param setting body models.RateLimitSetting true "Rate limit object"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/rate-limits/{id} [put]
|
||||||
|
func (h *SettingsHandler) UpdateRateLimit(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
var input map[string]interface{}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateRateLimitSetting(id, input); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Rate limit updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateCorsCache godoc
|
||||||
|
// @Summary Invalidate CORS cache (Admin)
|
||||||
|
// @Description Clear the CORS cache to force reload from database
|
||||||
|
// @Tags admin,settings
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Router /api/v1/admin/cors/cache/invalidate [post]
|
||||||
|
func (h *SettingsHandler) InvalidateCorsCache(c *gin.Context) {
|
||||||
|
h.service.InvalidateCorsCache()
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "CORS cache invalidated successfully"})
|
||||||
|
}
|
||||||
204
app/settings/services/cache_service.go
Normal file
204
app/settings/services/cache_service.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CacheService struct{}
|
||||||
|
|
||||||
|
func NewCacheService() *CacheService {
|
||||||
|
return &CacheService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User Cache
|
||||||
|
func (s *CacheService) SetUser(userID string, user *models.User, expiration time.Duration) error {
|
||||||
|
userData, err := json.Marshal(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return database.Set("user:"+userID, userData, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) GetUser(userID string) (*models.User, error) {
|
||||||
|
data, err := database.Get("user:" + userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err = json.Unmarshal([]byte(data), &user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) DeleteUser(userID string) error {
|
||||||
|
return database.Delete("user:" + userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session Management
|
||||||
|
func (s *CacheService) SetSession(token string, userID string, expiration time.Duration) error {
|
||||||
|
return database.Set("session:"+token, userID, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) GetSession(token string) (string, error) {
|
||||||
|
return database.Get("session:" + token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) DeleteSession(token string) error {
|
||||||
|
return database.Delete("session:" + token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate Limiting
|
||||||
|
func (s *CacheService) IncrementRateLimit(key string, expiration time.Duration) (int64, error) {
|
||||||
|
count, err := database.Increment("ratelimit:" + key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set expiration only for first increment
|
||||||
|
if count == 1 {
|
||||||
|
database.Expire("ratelimit:"+key, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) GetRateLimit(key string) (string, error) {
|
||||||
|
return database.Get("ratelimit:" + key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token Blacklist (for logout)
|
||||||
|
func (s *CacheService) BlacklistToken(token string, expiration time.Duration) error {
|
||||||
|
return database.Set("blacklist:"+token, "1", expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) IsTokenBlacklisted(token string) (bool, error) {
|
||||||
|
return database.Exists("blacklist:" + token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email Verification Token Cache
|
||||||
|
func (s *CacheService) SetEmailVerification(email string, token string, expiration time.Duration) error {
|
||||||
|
return database.Set("email_verify:"+email, token, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) GetEmailVerification(email string) (string, error) {
|
||||||
|
return database.Get("email_verify:" + email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) DeleteEmailVerification(email string) error {
|
||||||
|
return database.Delete("email_verify:" + email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password Reset Token Cache
|
||||||
|
func (s *CacheService) SetPasswordReset(email string, token string, expiration time.Duration) error {
|
||||||
|
return database.Set("password_reset:"+email, token, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) GetPasswordReset(email string) (string, error) {
|
||||||
|
return database.Get("password_reset:" + email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) DeletePasswordReset(email string) error {
|
||||||
|
return database.Delete("password_reset:" + email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS Whitelist Cache
|
||||||
|
func (s *CacheService) SetCorsWhitelist(origins []string, expiration time.Duration) error {
|
||||||
|
data, err := json.Marshal(origins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return database.Set("cors:whitelist", data, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) GetCorsWhitelist() ([]string, error) {
|
||||||
|
data, err := database.Get("cors:whitelist")
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var origins []string
|
||||||
|
err = json.Unmarshal([]byte(data), &origins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return origins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) InvalidateCorsWhitelist() error {
|
||||||
|
return database.Delete("cors:whitelist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS Blacklist Cache
|
||||||
|
func (s *CacheService) SetCorsBlacklist(origins []string, expiration time.Duration) error {
|
||||||
|
data, err := json.Marshal(origins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return database.Set("cors:blacklist", data, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) GetCorsBlacklist() ([]string, error) {
|
||||||
|
data, err := database.Get("cors:blacklist")
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var origins []string
|
||||||
|
err = json.Unmarshal([]byte(data), &origins)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return origins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) InvalidateCorsBlacklist() error {
|
||||||
|
return database.Delete("cors:blacklist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate Limit Settings Cache
|
||||||
|
func (s *CacheService) SetRateLimitSettings(settings map[string]*models.RateLimitSetting, expiration time.Duration) error {
|
||||||
|
data, err := json.Marshal(settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return database.Set("settings:ratelimit", data, expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) GetRateLimitSettings() (map[string]*models.RateLimitSetting, error) {
|
||||||
|
data, err := database.Get("settings:ratelimit")
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings map[string]*models.RateLimitSetting
|
||||||
|
err = json.Unmarshal([]byte(data), &settings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CacheService) InvalidateRateLimitSettings() error {
|
||||||
|
return database.Delete("settings:ratelimit")
|
||||||
|
}
|
||||||
173
app/settings/services/jwt_service.go
Normal file
173
app/settings/services/jwt_service.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gobeyhan/config"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JWTClaim struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Permissions []string `json:"permissions,omitempty"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWTService struct{}
|
||||||
|
|
||||||
|
func NewJWTService() *JWTService {
|
||||||
|
return &JWTService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *JWTService) GenerateToken(user models.User) (string, error) {
|
||||||
|
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
|
||||||
|
return "", errors.New("jwt secret not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationTime := time.Now().Add(24 * time.Hour)
|
||||||
|
claims := &JWTClaim{
|
||||||
|
UserID: fmt.Sprintf("%d", user.ID),
|
||||||
|
Email: user.Email,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||||
|
Issuer: "gauth-central",
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString([]byte(config.AppConfig.JWTSecret))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *JWTService) GenerateTokenPair(user models.User) (string, string, error) {
|
||||||
|
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
|
||||||
|
return "", "", errors.New("jwt secret not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract permissions
|
||||||
|
permissionMap := make(map[string]bool)
|
||||||
|
for _, role := range user.Roles {
|
||||||
|
for _, perm := range role.Permissions {
|
||||||
|
permissionMap[perm.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var permissions []string
|
||||||
|
for p := range permissionMap {
|
||||||
|
permissions = append(permissions, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access Token
|
||||||
|
expirationMinutes := 120 // Default fallback
|
||||||
|
if config.AppConfig.AccessTokenExpireMinutes > 0 {
|
||||||
|
expirationMinutes = config.AppConfig.AccessTokenExpireMinutes
|
||||||
|
}
|
||||||
|
accessTokenExp := time.Now().Add(time.Duration(expirationMinutes) * time.Minute)
|
||||||
|
accessClaims := &JWTClaim{
|
||||||
|
UserID: fmt.Sprintf("%d", user.ID),
|
||||||
|
Email: user.Email,
|
||||||
|
Permissions: permissions,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
Subject: fmt.Sprintf("%d", user.ID), // CRITICAL: Standard "sub" claim
|
||||||
|
ExpiresAt: jwt.NewNumericDate(accessTokenExp),
|
||||||
|
Issuer: "gauth-central",
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
||||||
|
signedAccessToken, err := accessToken.SignedString([]byte(config.AppConfig.JWTSecret))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh Token
|
||||||
|
expirationDays := 30 // Default fallback
|
||||||
|
if config.AppConfig.RefreshTokenExpireDays > 0 {
|
||||||
|
expirationDays = config.AppConfig.RefreshTokenExpireDays
|
||||||
|
}
|
||||||
|
refreshTokenExp := time.Now().Add(time.Duration(expirationDays) * 24 * time.Hour)
|
||||||
|
refreshClaims := &JWTClaim{
|
||||||
|
UserID: fmt.Sprintf("%d", user.ID),
|
||||||
|
Email: user.Email,
|
||||||
|
Permissions: nil, // Refresh token doesn't need permissions usually, or keep them if needed
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
Subject: fmt.Sprintf("%d", user.ID), // CRITICAL: Standard "sub" claim
|
||||||
|
ExpiresAt: jwt.NewNumericDate(refreshTokenExp),
|
||||||
|
Issuer: "gauth-central",
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
||||||
|
signedRefreshToken, err := refreshToken.SignedString([]byte(config.AppConfig.JWTSecret))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedAccessToken, signedRefreshToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *JWTService) ValidateToken(signedToken string) (*JWTClaim, error) {
|
||||||
|
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
|
||||||
|
return nil, errors.New("jwt secret not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.ParseWithClaims(
|
||||||
|
signedToken,
|
||||||
|
&JWTClaim{},
|
||||||
|
func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, errors.New("unexpected signing method")
|
||||||
|
}
|
||||||
|
return []byte(config.AppConfig.JWTSecret), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*JWTClaim)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("could not parse claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.ExpiresAt.Time.Before(time.Now()) {
|
||||||
|
return nil, errors.New("token expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateVerificationToken generates a JWT token for email verification (24 hours expiry)
|
||||||
|
func (s *JWTService) GenerateVerificationToken(userID, email string) (string, error) {
|
||||||
|
if config.AppConfig == nil || config.AppConfig.JWTSecret == "" {
|
||||||
|
return "", errors.New("jwt secret not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
expirationTime := time.Now().Add(24 * time.Hour)
|
||||||
|
claims := &JWTClaim{
|
||||||
|
UserID: userID,
|
||||||
|
Email: email,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||||
|
Issuer: "gauth-central",
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString([]byte(config.AppConfig.JWTSecret))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateVerificationToken validates a verification token and returns user ID and email
|
||||||
|
func (s *JWTService) ValidateVerificationToken(tokenString string) (string, string, error) {
|
||||||
|
claims, err := s.ValidateToken(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return claims.UserID, claims.Email, nil
|
||||||
|
}
|
||||||
391
app/settings/services/settings_service.go
Normal file
391
app/settings/services/settings_service.go
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SettingsService struct {
|
||||||
|
cacheService *CacheService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettingsService() *SettingsService {
|
||||||
|
return &SettingsService{
|
||||||
|
cacheService: NewCacheService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CORS WHITELIST ====================
|
||||||
|
|
||||||
|
func (s *SettingsService) GetAllCorsWhitelist() ([]models.CorsWhitelist, error) {
|
||||||
|
var whitelists []models.CorsWhitelist
|
||||||
|
err := database.DB.Where("is_active = ?", true).Order("created_at DESC").Find(&whitelists).Error
|
||||||
|
return whitelists, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) GetActiveWhitelistOrigins() ([]string, error) {
|
||||||
|
// Try cache first
|
||||||
|
cached, err := s.cacheService.GetCorsWhitelist()
|
||||||
|
if err == nil && cached != nil {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
origins, err := s.getActiveWhitelistOriginsFromDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache for 1 hour
|
||||||
|
s.cacheService.SetCorsWhitelist(origins, 1*time.Hour)
|
||||||
|
|
||||||
|
return origins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrCorsOriginExists = errors.New("cors origin already exists")
|
||||||
|
|
||||||
|
func (s *SettingsService) CreateCorsWhitelist(whitelist *models.CorsWhitelist) error {
|
||||||
|
var existing models.CorsWhitelist
|
||||||
|
err := database.DB.Where("LOWER(origin) = LOWER(?)", whitelist.Origin).First(&existing).Error
|
||||||
|
if err == nil {
|
||||||
|
if existing.IsActive {
|
||||||
|
return ErrCorsOriginExists
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"is_active": true,
|
||||||
|
"description": whitelist.Description,
|
||||||
|
"created_by": whitelist.CreatedBy,
|
||||||
|
}
|
||||||
|
err = database.DB.Model(&models.CorsWhitelist{}).Where("id = ?", existing.ID).Updates(updates).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.InvalidateCorsCache()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = database.DB.Create(whitelist).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
s.InvalidateCorsCache()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) UpdateCorsWhitelist(id string, updates map[string]interface{}) error {
|
||||||
|
err := database.DB.Model(&models.CorsWhitelist{}).Where("id = ?", id).Updates(updates).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
s.InvalidateCorsCache()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) DeleteCorsWhitelist(id string) error {
|
||||||
|
err := database.DB.Delete(&models.CorsWhitelist{}, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
s.InvalidateCorsCache()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) GetCorsWhitelistByID(id uint64) (*models.CorsWhitelist, error) {
|
||||||
|
var item models.CorsWhitelist
|
||||||
|
err := database.DB.First(&item, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CORS BLACKLIST ====================
|
||||||
|
|
||||||
|
func (s *SettingsService) GetAllCorsBlacklist() ([]models.CorsBlacklist, error) {
|
||||||
|
var blacklists []models.CorsBlacklist
|
||||||
|
err := database.DB.Where("is_active = ?", true).Order("created_at DESC").Find(&blacklists).Error
|
||||||
|
return blacklists, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) GetActiveBlacklistOrigins() ([]string, error) {
|
||||||
|
// Try cache first
|
||||||
|
cached, err := s.cacheService.GetCorsBlacklist()
|
||||||
|
if err == nil && cached != nil {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
origins, err := s.getActiveBlacklistOriginsFromDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache for 1 hour
|
||||||
|
s.cacheService.SetCorsBlacklist(origins, 1*time.Hour)
|
||||||
|
|
||||||
|
return origins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) CreateCorsBlacklist(blacklist *models.CorsBlacklist) error {
|
||||||
|
err := database.DB.Create(blacklist).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
s.InvalidateCorsCache()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) UpdateCorsBlacklist(id string, updates map[string]interface{}) error {
|
||||||
|
err := database.DB.Model(&models.CorsBlacklist{}).Where("id = ?", id).Updates(updates).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
s.InvalidateCorsCache()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) DeleteCorsBlacklist(id string) error {
|
||||||
|
err := database.DB.Delete(&models.CorsBlacklist{}, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
s.InvalidateCorsCache()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) GetCorsBlacklistByID(id uint64) (*models.CorsBlacklist, error) {
|
||||||
|
var item models.CorsBlacklist
|
||||||
|
err := database.DB.First(&item, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RATE LIMIT SETTINGS ====================
|
||||||
|
|
||||||
|
func (s *SettingsService) GetAllRateLimitSettings() ([]models.RateLimitSetting, error) {
|
||||||
|
var settings []models.RateLimitSetting
|
||||||
|
err := database.DB.Order("name ASC").Find(&settings).Error
|
||||||
|
return settings, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) GetRateLimitSettingsMap() (map[string]*models.RateLimitSetting, error) {
|
||||||
|
// Try cache first
|
||||||
|
cached, err := s.cacheService.GetRateLimitSettings()
|
||||||
|
if err == nil && cached != nil {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from database
|
||||||
|
var settings []models.RateLimitSetting
|
||||||
|
err = database.DB.Where("is_active = ?", true).Find(&settings).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsMap := make(map[string]*models.RateLimitSetting)
|
||||||
|
for i := range settings {
|
||||||
|
settingsMap[settings[i].Name] = &settings[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache for 1 hour
|
||||||
|
s.cacheService.SetRateLimitSettings(settingsMap, 1*time.Hour)
|
||||||
|
|
||||||
|
return settingsMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) GetRateLimitSettingByName(name string) (*models.RateLimitSetting, error) {
|
||||||
|
settingsMap, err := s.GetRateLimitSettingsMap()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
setting, exists := settingsMap[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return setting, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) UpdateRateLimitSetting(id string, updates map[string]interface{}) error {
|
||||||
|
err := database.DB.Model(&models.RateLimitSetting{}).Where("id = ?", id).Updates(updates).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
s.cacheService.InvalidateRateLimitSettings()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) GetRateLimitSettingByID(id uint64) (*models.RateLimitSetting, error) {
|
||||||
|
var item models.RateLimitSetting
|
||||||
|
err := database.DB.First(&item, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) DeleteRateLimitSetting(id string) error {
|
||||||
|
err := database.DB.Delete(&models.RateLimitSetting{}, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate cache
|
||||||
|
s.cacheService.InvalidateRateLimitSettings()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate CORS caches (whitelist + blacklist)
|
||||||
|
func (s *SettingsService) InvalidateCorsCache() {
|
||||||
|
s.cacheService.InvalidateCorsWhitelist()
|
||||||
|
s.cacheService.InvalidateCorsBlacklist()
|
||||||
|
log.Println("cors_cache_invalidated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if origin is allowed
|
||||||
|
func (s *SettingsService) IsOriginAllowed(origin string) (bool, error) {
|
||||||
|
allowed, _, _, err := s.CheckOrigin(origin)
|
||||||
|
return allowed, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckOrigin returns decision details for debug logging.
|
||||||
|
func (s *SettingsService) CheckOrigin(origin string) (bool, string, string, error) {
|
||||||
|
// Check blacklist first
|
||||||
|
blacklist, err := s.GetActiveBlacklistOrigins()
|
||||||
|
if err != nil {
|
||||||
|
return false, "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, blocked := range blacklist {
|
||||||
|
if originMatchesEntry(origin, blocked) {
|
||||||
|
return false, blocked, "blacklist", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: refresh blacklist on miss (stale cache protection)
|
||||||
|
freshBlacklist, err := s.getActiveBlacklistOriginsFromDB()
|
||||||
|
if err != nil {
|
||||||
|
return false, "", "", err
|
||||||
|
}
|
||||||
|
if len(freshBlacklist) != 0 {
|
||||||
|
s.cacheService.SetCorsBlacklist(freshBlacklist, 1*time.Hour)
|
||||||
|
}
|
||||||
|
for _, blocked := range freshBlacklist {
|
||||||
|
if originMatchesEntry(origin, blocked) {
|
||||||
|
return false, blocked, "blacklist", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whitelist
|
||||||
|
whitelist, err := s.GetActiveWhitelistOrigins()
|
||||||
|
if err != nil {
|
||||||
|
return false, "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, allowed := range whitelist {
|
||||||
|
if allowed == "*" || originMatchesEntry(origin, allowed) {
|
||||||
|
return true, allowed, "whitelist", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: refresh whitelist on miss (stale cache protection)
|
||||||
|
freshWhitelist, err := s.getActiveWhitelistOriginsFromDB()
|
||||||
|
if err != nil {
|
||||||
|
return false, "", "", err
|
||||||
|
}
|
||||||
|
if len(freshWhitelist) != 0 {
|
||||||
|
s.cacheService.SetCorsWhitelist(freshWhitelist, 1*time.Hour)
|
||||||
|
}
|
||||||
|
for _, allowed := range freshWhitelist {
|
||||||
|
if allowed == "*" || originMatchesEntry(origin, allowed) {
|
||||||
|
return true, allowed, "whitelist", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "", "whitelist", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) getActiveWhitelistOriginsFromDB() ([]string, error) {
|
||||||
|
var whitelists []models.CorsWhitelist
|
||||||
|
err := database.DB.Where("is_active = ?", true).Find(&whitelists).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
origins := make([]string, len(whitelists))
|
||||||
|
for i, w := range whitelists {
|
||||||
|
origins[i] = w.Origin
|
||||||
|
}
|
||||||
|
|
||||||
|
return origins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) getActiveBlacklistOriginsFromDB() ([]string, error) {
|
||||||
|
var blacklists []models.CorsBlacklist
|
||||||
|
err := database.DB.Where("is_active = ?", true).Find(&blacklists).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
origins := make([]string, len(blacklists))
|
||||||
|
for i, b := range blacklists {
|
||||||
|
origins[i] = b.Origin
|
||||||
|
}
|
||||||
|
|
||||||
|
return origins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func originMatchesEntry(origin string, entry string) bool {
|
||||||
|
origin = strings.TrimSpace(origin)
|
||||||
|
entry = strings.TrimSpace(entry)
|
||||||
|
if origin == "" || entry == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
originLower := strings.ToLower(origin)
|
||||||
|
entryLower := strings.ToLower(entry)
|
||||||
|
if strings.Contains(entryLower, "://") {
|
||||||
|
return originLower == entryLower
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := url.Parse(originLower)
|
||||||
|
if err != nil || parsed.Host == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
hostLower := strings.ToLower(parsed.Host)
|
||||||
|
if entryLower == hostLower {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow entries like "127.0.0.1" to match any port
|
||||||
|
hostOnly := strings.Split(hostLower, ":")[0]
|
||||||
|
return entryLower == hostOnly
|
||||||
|
}
|
||||||
59
client.http
Normal file
59
client.http
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
@baseUrl = http://localhost:8080/api/v1
|
||||||
|
@contentType = application/json
|
||||||
|
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyIiwiZW1haWwiOiJiZXloYW5AYmV5aGFuLmRldiIsImlzcyI6ImdhdXRoLWNlbnRyYWwiLCJleHAiOjE3NzA4NTY0MzcsImlhdCI6MTc3MDg0OTIzN30.z5iEjj7rgPy-SODJFN-PeEshAof0hYeqqrCqDUgaUuU
|
||||||
|
### Register
|
||||||
|
POST {{baseUrl}}/auth/register
|
||||||
|
Content-Type: {{contentType}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "beyhan@beyhan.dev",
|
||||||
|
"password": "1923btO**",
|
||||||
|
"username": "Beyhan Oğur"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Login (Admin)
|
||||||
|
# Default Admin Credentials:
|
||||||
|
# Email: admin@gauth.local
|
||||||
|
# Password: Admin@123
|
||||||
|
POST {{baseUrl}}/auth/login
|
||||||
|
Content-Type: {{contentType}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "admin@gauth.local",
|
||||||
|
"password": "Admin@123"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Login (User)
|
||||||
|
POST {{baseUrl}}/auth/login
|
||||||
|
Content-Type: {{contentType}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "beyhan@beyhan.dev",
|
||||||
|
"password": "1923btO**"
|
||||||
|
}
|
||||||
|
### Login (User)
|
||||||
|
POST {{baseUrl}}/auth/login
|
||||||
|
Content-Type: {{contentType}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "ares@ares.com",
|
||||||
|
"password": "1923btO**"
|
||||||
|
}
|
||||||
|
|
||||||
|
### OAuth Login (Google)
|
||||||
|
GET {{baseUrl}}/auth/google
|
||||||
|
|
||||||
|
### OAuth Login (GitHub)
|
||||||
|
GET {{baseUrl}}/auth/github
|
||||||
|
|
||||||
|
### Logout
|
||||||
|
POST {{baseUrl}}/auth/logout
|
||||||
|
Content-Type: {{contentType}}
|
||||||
|
|
||||||
|
### Get Categories (Public)
|
||||||
|
GET {{baseUrl}}/categories
|
||||||
|
|
||||||
|
### Get Current User (Protected)
|
||||||
|
# Replace @token with the actual token received from login
|
||||||
|
GET {{baseUrl}}/auth/me
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
83
cmd/server/main.go
Normal file
83
cmd/server/main.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"gobeyhan/app/routes"
|
||||||
|
"gobeyhan/config"
|
||||||
|
"gobeyhan/database"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
|
||||||
|
_ "gobeyhan/docs" // Swagger docs
|
||||||
|
)
|
||||||
|
|
||||||
|
// @title Beyhan Backend API
|
||||||
|
// @version 2.0
|
||||||
|
// @description Modular REST API with Blog, Account, and Settings apps
|
||||||
|
// @termsOfService http://swagger.io/terms/
|
||||||
|
|
||||||
|
// @contact.name API Support
|
||||||
|
// @contact.email support@beyhan.com
|
||||||
|
|
||||||
|
// @license.name MIT
|
||||||
|
// @license.url https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// @host localhost:8080
|
||||||
|
// @BasePath /api/v1
|
||||||
|
|
||||||
|
// @securityDefinitions.apikey BearerAuth
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
// @description Type "Bearer" followed by a space and JWT token.
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Load configuration
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
// Connect to database
|
||||||
|
database.ConnectDB()
|
||||||
|
|
||||||
|
// Connect to Redis
|
||||||
|
database.ConnectRedis()
|
||||||
|
if err := database.FlushAll(); err != nil {
|
||||||
|
log.Printf("Warning: Failed to flush Redis cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migrations in development
|
||||||
|
if config.AppConfig.Env == "development" {
|
||||||
|
if err := database.Migrate(database.DB); err != nil {
|
||||||
|
log.Fatalf("Migration failed: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("Migration complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Gin
|
||||||
|
if config.AppConfig.Env == "production" {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// Swagger endpoint
|
||||||
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
|
||||||
|
// Setup routes
|
||||||
|
routes.SetupRoutes(r)
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
port := config.AppConfig.Port
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("🚀 Server starting on port %s", port)
|
||||||
|
log.Printf("📚 Swagger UI: http://localhost:%s/swagger/index.html", port)
|
||||||
|
log.Printf("🌐 API Base: http://localhost:%s/api/v1", port)
|
||||||
|
|
||||||
|
if err := r.Run(":" + port); err != nil {
|
||||||
|
log.Fatalf("Failed to start server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
232
config/config.go
Normal file
232
config/config.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Env string // e.g. development, production
|
||||||
|
Port string
|
||||||
|
DBUrl string
|
||||||
|
JWTSecret string
|
||||||
|
AppURL string // e.g. https://api.example.com - used for verification links in emails
|
||||||
|
GoogleClientID string
|
||||||
|
GoogleClientSecret string
|
||||||
|
GithubClientID string
|
||||||
|
GithubClientSecret string
|
||||||
|
GoogleRedirectURL string
|
||||||
|
GithubRedirectURL string
|
||||||
|
ClientCallbackURL string
|
||||||
|
OAuthRedirectURL string
|
||||||
|
RedisUrl string
|
||||||
|
AccessTokenExpireMinutes int
|
||||||
|
RefreshTokenExpireDays int
|
||||||
|
|
||||||
|
// Avatar Settings
|
||||||
|
AvatarHeight int
|
||||||
|
AvatarWidth int
|
||||||
|
AvatarQuality int
|
||||||
|
AvatarFormat string
|
||||||
|
AvatarMode string // cover, contain, resize
|
||||||
|
|
||||||
|
// Home Image Settings
|
||||||
|
HomeImageHeight int
|
||||||
|
HomeImageWidth int
|
||||||
|
HomeImageQuality int
|
||||||
|
HomeImageFormat string
|
||||||
|
HomeImageMode string // cover, contain, resize
|
||||||
|
|
||||||
|
// About Image Settings
|
||||||
|
AboutImageHeight int
|
||||||
|
AboutImageWidth int
|
||||||
|
AboutImageQuality int
|
||||||
|
AboutImageFormat string
|
||||||
|
AboutImageMode string // cover, contain, resize
|
||||||
|
|
||||||
|
// Service Image Settings
|
||||||
|
ServiceImageHeight int
|
||||||
|
ServiceImageWidth int
|
||||||
|
ServiceImageQuality int
|
||||||
|
ServiceImageFormat string
|
||||||
|
ServiceImageMode string // cover, contain, resize
|
||||||
|
|
||||||
|
// Post Image Settings
|
||||||
|
PostImageHeight int
|
||||||
|
PostImageWidth int
|
||||||
|
PostImageQuality int
|
||||||
|
PostImageFormat string
|
||||||
|
PostImageMode string // cover, contain, resize
|
||||||
|
|
||||||
|
// Post Category Image Settings
|
||||||
|
PostCategoryImageHeight int
|
||||||
|
PostCategoryImageWidth int
|
||||||
|
PostCategoryImageQuality int
|
||||||
|
PostCategoryImageFormat string
|
||||||
|
PostCategoryImageMode string // cover, contain, resize
|
||||||
|
|
||||||
|
// Settings Logo Settings
|
||||||
|
SettingsLogoHeight int
|
||||||
|
SettingsLogoWidth int
|
||||||
|
SettingsLogoQuality int
|
||||||
|
SettingsLogoFormat string
|
||||||
|
SettingsLogoMode string // cover, contain, resize
|
||||||
|
|
||||||
|
// Banner Image Settings
|
||||||
|
BannerImageHeight int
|
||||||
|
BannerImageWidth int
|
||||||
|
BannerImageQuality int
|
||||||
|
BannerImageFormat string
|
||||||
|
BannerImageMode string // cover, contain, resize
|
||||||
|
|
||||||
|
// Banner Thumb Settings
|
||||||
|
BannerThumbHeight int
|
||||||
|
BannerThumbWidth int
|
||||||
|
BannerThumbQuality int
|
||||||
|
BannerThumbFormat string
|
||||||
|
BannerThumbMode string // cover, contain, resize
|
||||||
|
|
||||||
|
// Email Settings
|
||||||
|
EmailHost string
|
||||||
|
EmailPort string
|
||||||
|
EmailHostUser string
|
||||||
|
EmailHostPassword string
|
||||||
|
EmailFrom string
|
||||||
|
CorsDebug bool
|
||||||
|
TurnstileSecretKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
var AppConfig *Config
|
||||||
|
|
||||||
|
func LoadConfig() {
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Warning: Error loading .env file, continuing with system env")
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig = &Config{
|
||||||
|
Env: getEnv("APP_ENV", "development"),
|
||||||
|
Port: getEnv("PORT", "8080"),
|
||||||
|
DBUrl: getEnv("DB_URL", ""),
|
||||||
|
JWTSecret: getEnv("JWT_SECRET", "default_secret"),
|
||||||
|
AppURL: getEnv("APP_URL", "http://localhost:8080"),
|
||||||
|
GoogleClientID: getEnv("GOOGLE_CLIENT_ID", ""),
|
||||||
|
GoogleClientSecret: getEnv("GOOGLE_CLIENT_SECRET", ""),
|
||||||
|
GithubClientID: getEnv("GITHUB_CLIENT_ID", ""),
|
||||||
|
GithubClientSecret: getEnv("GITHUB_CLIENT_SECRET", ""),
|
||||||
|
GoogleRedirectURL: getEnv("GOOGLE_REDIRECT_URL", "http://localhost:8080/api/v1/auth/google/callback"),
|
||||||
|
GithubRedirectURL: getEnv("GITHUB_REDIRECT_URL", "http://localhost:8080/api/v1/auth/github/callback"),
|
||||||
|
ClientCallbackURL: getEnv("CLIENT_CALLBACK_URL", ""),
|
||||||
|
OAuthRedirectURL: getEnv("OAUTH_REDIRECT_URL", ""),
|
||||||
|
RedisUrl: getEnv("REDIS_URL", ""),
|
||||||
|
AccessTokenExpireMinutes: getEnvAsInt("ACCESS_TOKEN_EXPIRE_MINUTES", 120), // Default 120 minutes
|
||||||
|
RefreshTokenExpireDays: getEnvAsInt("REFRESH_TOKEN_EXPIRE_DAYS", 30), // Default 30 days
|
||||||
|
|
||||||
|
// Avatar Defaults
|
||||||
|
AvatarHeight: getEnvAsInt("AVATAR_H", 0), // Default 0 (auto)
|
||||||
|
AvatarWidth: getEnvAsInt("AVATAR_W", 800), // Default 800
|
||||||
|
AvatarQuality: getEnvAsInt("AVATAR_Q", 80), // Default 80
|
||||||
|
AvatarFormat: getEnv("AVATAR_F", "webp"), // Default webp
|
||||||
|
AvatarMode: getEnv("AVATAR_B", "contain"), // Default contain (Fit)
|
||||||
|
|
||||||
|
// Home Image Defaults
|
||||||
|
HomeImageHeight: getEnvAsInt("HOME_IMAGE_H", 0), // Default 0 (auto)
|
||||||
|
HomeImageWidth: getEnvAsInt("HOME_IMAGE_W", 800), // Default 800
|
||||||
|
HomeImageQuality: getEnvAsInt("HOME_IMAGE_Q", 80), // Default 80
|
||||||
|
HomeImageFormat: getEnv("HOME_IMAGE_F", "webp"), // Default webp
|
||||||
|
HomeImageMode: getEnv("HOME_IMAGE_B", "contain"), // Default contain (Fit)
|
||||||
|
|
||||||
|
// About Image Defaults
|
||||||
|
AboutImageHeight: getEnvAsInt("ABOUTME_IMAGE_H", getEnvAsInt("ABOUT_IMAGE_H", 0)),
|
||||||
|
AboutImageWidth: getEnvAsInt("ABOUTME_IMAGE_W", getEnvAsInt("ABOUT_IMAGE_W", 800)),
|
||||||
|
AboutImageQuality: getEnvAsInt("ABOUTME_IMAGE_Q", getEnvAsInt("ABOUT_IMAGE_Q", 80)),
|
||||||
|
AboutImageFormat: getEnv("ABOUTME_IMAGE_F", getEnv("ABOUT_IMAGE_F", "webp")),
|
||||||
|
AboutImageMode: getEnv("ABOUTME_IMAGE_B", getEnv("ABOUT_IMAGE_B", "contain")),
|
||||||
|
|
||||||
|
// Service Image Defaults
|
||||||
|
ServiceImageHeight: getEnvAsInt("SERVICE_IMAGE_H", 256),
|
||||||
|
ServiceImageWidth: getEnvAsInt("SERVICE_IMAGE_W", 256),
|
||||||
|
ServiceImageQuality: getEnvAsInt("SERVICE_IMAGE_Q", 90),
|
||||||
|
ServiceImageFormat: getEnv("SERVICE_IMAGE_F", "png"),
|
||||||
|
ServiceImageMode: getEnv("SERVICE_IMAGE_B", "cover"),
|
||||||
|
|
||||||
|
// Post Image Defaults
|
||||||
|
PostImageHeight: getEnvAsInt("POST_IMAGE_H", 450),
|
||||||
|
PostImageWidth: getEnvAsInt("POST_IMAGE_W", 700),
|
||||||
|
PostImageQuality: getEnvAsInt("POST_IMAGE_Q", 90),
|
||||||
|
PostImageFormat: getEnv("POST_IMAGE_F", "webp"),
|
||||||
|
PostImageMode: getEnv("POST_IMAGE_B", "cover"),
|
||||||
|
|
||||||
|
// Post Category Image Defaults
|
||||||
|
PostCategoryImageHeight: getEnvAsInt("POST_CATEGORY_IMAGE_H", 300),
|
||||||
|
PostCategoryImageWidth: getEnvAsInt("POST_CATEGORY_IMAGE_W", 300),
|
||||||
|
PostCategoryImageQuality: getEnvAsInt("POST_CATEGORY_IMAGE_Q", 85),
|
||||||
|
PostCategoryImageFormat: getEnv("POST_CATEGORY_IMAGE_F", "png"),
|
||||||
|
PostCategoryImageMode: getEnv("POST_CATEGORY_IMAGE_B", "cover"),
|
||||||
|
|
||||||
|
// Settings Logo Defaults
|
||||||
|
SettingsLogoHeight: getEnvAsInt("SETTINGS_LOGO_H", 54),
|
||||||
|
SettingsLogoWidth: getEnvAsInt("SETTINGS_LOGO_W", 165),
|
||||||
|
SettingsLogoQuality: getEnvAsInt("SETTINGS_LOGO_Q", 85),
|
||||||
|
SettingsLogoFormat: getEnv("SETTINGS_LOGO_F", "png"),
|
||||||
|
SettingsLogoMode: getEnv("SETTINGS_LOGO_B", "cover"),
|
||||||
|
|
||||||
|
// Banner Image Defaults
|
||||||
|
BannerImageHeight: getEnvAsInt("BANNER_IMAGE_H", 700),
|
||||||
|
BannerImageWidth: getEnvAsInt("BANNER_IMAGE_W", 1920),
|
||||||
|
BannerImageQuality: getEnvAsInt("BANNER_IMAGE_Q", 85),
|
||||||
|
BannerImageFormat: getEnv("BANNER_IMAGE_F", "webp"),
|
||||||
|
BannerImageMode: getEnv("BANNER_IMAGE_B", "cover"),
|
||||||
|
|
||||||
|
// Banner Thumb Defaults
|
||||||
|
BannerThumbHeight: getEnvAsInt("BANNER_THUMB_H", 48),
|
||||||
|
BannerThumbWidth: getEnvAsInt("BANNER_THUMB_W", 48),
|
||||||
|
BannerThumbQuality: getEnvAsInt("BANNER_THUMB_Q", 90),
|
||||||
|
BannerThumbFormat: getEnv("BANNER_THUMB_F", "png"),
|
||||||
|
BannerThumbMode: getEnv("BANNER_THUMB_B", "cover"),
|
||||||
|
|
||||||
|
// Email Settings
|
||||||
|
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),
|
||||||
|
TurnstileSecretKey: getEnv("CLOUD_FLARE_SECRET", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
31
database/README_migrate.md
Normal file
31
database/README_migrate.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Kısa kullanım
|
||||||
|
|
||||||
|
Bu proje için GORM AutoMigrate helper'ı `Migrate(db *gorm.DB) error` fonksiyonu olarak sağlanmıştır.
|
||||||
|
|
||||||
|
Örnek kullanım (ör. `main.go` içinde):
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"gobeyhan/config" // DB konfigürasyonunuza göre düzenleyin
|
||||||
|
"gobeyhan/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db, err := config.NewDB() // veya projenizdeki DB bağlantı fonksiyonu
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.Migrate(db); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// uygulama başlat
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notlar:
|
||||||
|
- `database/migrate.go` sadece modeller için `AutoMigrate` çağrısını yapar.
|
||||||
|
- Thumbnail oluşturma ve dosya upload işlemleri model hook'larında değil upload handler'larında yapılmalıdır.
|
||||||
|
- Eğer DB seviyesinde benzersiz constraint'ler isterseniz, GORM tag veya migration dosyası ile `uniqueIndex` ekleyin.
|
||||||
|
|
||||||
329
database/db.go
Normal file
329
database/db.go
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
//"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gobeyhan/config"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
func ConnectDB() {
|
||||||
|
dsn := config.AppConfig.DBUrl
|
||||||
|
if dsn == "" {
|
||||||
|
log.Fatal("DB_URL is not set in .env")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure GORM with optimized settings
|
||||||
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Info), // Only log errors, suppress SLOW SQL warnings
|
||||||
|
PrepareStmt: true, // Prepare statements for better performance
|
||||||
|
NowFunc: func() time.Time {
|
||||||
|
return time.Now().UTC()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to connect to database:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Connected to Database successfully")
|
||||||
|
DB = db
|
||||||
|
|
||||||
|
// MySQL doesn't require enabling uuid-ossp extension. noop for compatibility
|
||||||
|
onEnableUUIDForMySQL()
|
||||||
|
}
|
||||||
|
|
||||||
|
func onEnableUUIDForMySQL() {
|
||||||
|
// noop: Postgres-only extension; for MySQL UUID handling is usually done at application level
|
||||||
|
log.Println("UUID extension step skipped for MySQL (not required)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func SeedAll() {
|
||||||
|
if DB == nil {
|
||||||
|
log.Println("DB not initialized: call ConnectDB() before SeedAll")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run AutoMigrate using the helper in migrate.go
|
||||||
|
if err := Migrate(DB); err != nil {
|
||||||
|
log.Printf("AutoMigrate failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run schema/data migrations
|
||||||
|
migrateUserNameColumn()
|
||||||
|
migrateEmailVerifiedColumn()
|
||||||
|
|
||||||
|
// Seed initial data
|
||||||
|
seedRolesAndPermissions()
|
||||||
|
seedDefaultSettings()
|
||||||
|
SeedDefaultAdmin()
|
||||||
|
|
||||||
|
log.Println("Database migration and seeding complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateEmailVerifiedColumn() {
|
||||||
|
// Check column existence via information_schema for MySQL
|
||||||
|
var count int64
|
||||||
|
DB.Raw(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'email_verified'
|
||||||
|
`).Scan(&count)
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
// Column doesn't exist, nothing to migrate
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only set existing users (created before email verification feature) as verified
|
||||||
|
var usersToVerify int64
|
||||||
|
DB.Model(&models.User{}).Where("(email_verify_token IS NULL OR email_verify_token = '') AND email_verified IS NULL").Count(&usersToVerify)
|
||||||
|
|
||||||
|
if usersToVerify > 0 {
|
||||||
|
DB.Exec("UPDATE users SET email_verified = true WHERE (email_verify_token IS NULL OR email_verify_token = '') AND email_verified IS NULL")
|
||||||
|
log.Printf("Email verification migration: %d existing users marked as verified", usersToVerify)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateUserNameColumn() {
|
||||||
|
// Check column existence via information_schema for MySQL
|
||||||
|
var count int64
|
||||||
|
DB.Raw(`
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'user_name'
|
||||||
|
`).Scan(&count)
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
// Column doesn't exist, add it
|
||||||
|
log.Println("Adding user_name column...")
|
||||||
|
DB.Exec("ALTER TABLE users ADD COLUMN user_name TEXT")
|
||||||
|
|
||||||
|
// Update existing users with default usernames
|
||||||
|
DB.Exec("UPDATE users SET user_name = CONCAT('user_', SUBSTRING(CAST(id AS CHAR), 1, 8)) WHERE user_name IS NULL")
|
||||||
|
|
||||||
|
// Add NOT NULL constraint
|
||||||
|
DB.Exec("ALTER TABLE users MODIFY COLUMN user_name TEXT NOT NULL")
|
||||||
|
log.Println("user_name column added successfully")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Column exists, update null or empty values
|
||||||
|
log.Println("Updating users with null or empty usernames...")
|
||||||
|
DB.Exec("UPDATE users SET user_name = CONCAT('user_', SUBSTRING(CAST(id AS CHAR), 1, 8)) WHERE user_name IS NULL OR user_name = ''")
|
||||||
|
|
||||||
|
// Check if NOT NULL constraint exists using information_schema
|
||||||
|
var isNullable string
|
||||||
|
DB.Raw(`
|
||||||
|
SELECT IS_NULLABLE
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'user_name'
|
||||||
|
`).Scan(&isNullable)
|
||||||
|
|
||||||
|
if strings.ToUpper(isNullable) != "NO" {
|
||||||
|
log.Println("Adding NOT NULL constraint to user_name...")
|
||||||
|
DB.Exec("ALTER TABLE users MODIFY COLUMN user_name TEXT NOT NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedRolesAndPermissions() {
|
||||||
|
// 1. Define Permissions
|
||||||
|
permissions := []models.Permission{
|
||||||
|
{Name: "user:read", Description: "Can read user data"},
|
||||||
|
{Name: "user:write", Description: "Can modify user data"},
|
||||||
|
{Name: "admin:access", Description: "Can access admin panel"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range permissions {
|
||||||
|
DB.FirstOrCreate(&models.Permission{}, models.Permission{Name: p.Name, Description: p.Description})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Define Roles
|
||||||
|
roles := []string{"admin", "user"}
|
||||||
|
for _, r := range roles {
|
||||||
|
DB.FirstOrCreate(&models.Role{}, models.Role{Name: r, Description: "Default " + r + " role"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Assign Permissions to Admin Role
|
||||||
|
var adminRole models.Role
|
||||||
|
DB.Preload("Permissions").Where("name = ?", "admin").First(&adminRole)
|
||||||
|
|
||||||
|
// Fetch all permissions to assign to admin
|
||||||
|
var allPermissions []models.Permission
|
||||||
|
DB.Find(&allPermissions)
|
||||||
|
|
||||||
|
// Update association (replace current set)
|
||||||
|
DB.Model(&adminRole).Association("Permissions").Replace(allPermissions)
|
||||||
|
|
||||||
|
// 4. Assign Basic Permissions to User Role
|
||||||
|
var userRole models.Role
|
||||||
|
DB.Preload("Permissions").Where("name = ?", "user").First(&userRole)
|
||||||
|
|
||||||
|
var userPermissions []models.Permission
|
||||||
|
DB.Where("name IN ?", []string{"user:read"}).Find(&userPermissions)
|
||||||
|
|
||||||
|
DB.Model(&userRole).Association("Permissions").Replace(userPermissions)
|
||||||
|
|
||||||
|
log.Println("Roles and Permissions seeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedDefaultSettings() {
|
||||||
|
// Seed default CORS whitelist
|
||||||
|
var whitelistCount int64
|
||||||
|
DB.Model(&models.CorsWhitelist{}).Count(&whitelistCount)
|
||||||
|
|
||||||
|
if whitelistCount == 0 {
|
||||||
|
defaultWhitelist := []models.CorsWhitelist{
|
||||||
|
{
|
||||||
|
Origin: "http://localhost:3000",
|
||||||
|
Description: "Default local frontend",
|
||||||
|
IsActive: true,
|
||||||
|
CreatedBy: "system",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Origin: "http://localhost:8080",
|
||||||
|
Description: "Backend self",
|
||||||
|
IsActive: true,
|
||||||
|
CreatedBy: "system",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, w := range defaultWhitelist {
|
||||||
|
DB.Create(&w)
|
||||||
|
}
|
||||||
|
log.Println("Default CORS whitelist seeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed default rate limit settings
|
||||||
|
var rateLimitCount int64
|
||||||
|
DB.Model(&models.RateLimitSetting{}).Count(&rateLimitCount)
|
||||||
|
|
||||||
|
if rateLimitCount == 0 {
|
||||||
|
defaultRateLimits := []models.RateLimitSetting{
|
||||||
|
{
|
||||||
|
Name: "login",
|
||||||
|
Description: "Login endpoint rate limit",
|
||||||
|
MaxRequests: 5,
|
||||||
|
WindowSeconds: 60, // 1 minute
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "register",
|
||||||
|
Description: "Registration endpoint rate limit",
|
||||||
|
MaxRequests: 3,
|
||||||
|
WindowSeconds: 300, // 5 minutes
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "api",
|
||||||
|
Description: "General API rate limit",
|
||||||
|
MaxRequests: 100,
|
||||||
|
WindowSeconds: 60, // 1 minute
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range defaultRateLimits {
|
||||||
|
DB.Create(&r)
|
||||||
|
}
|
||||||
|
log.Println("Default rate limit settings seeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedDefaultAdmin creates the default admin user if it doesn't exist
|
||||||
|
func SeedDefaultAdmin() {
|
||||||
|
if DB == nil {
|
||||||
|
log.Println("DB not initialized: call ConnectDB() before seeding")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a transaction to ensure atomic create + role assignment
|
||||||
|
tx := DB.Begin()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
log.Printf("panic during SeedDefaultAdmin: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Check if admin user already exists (including soft-deleted)
|
||||||
|
var adminUser models.User
|
||||||
|
err := tx.Unscoped().Where("email = ?", "admin@gauth.local").First(&adminUser).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Admin user doesn't exist, create one
|
||||||
|
// Hash default password: "Admin@123"
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("Admin@123"), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to hash admin password: %v", err)
|
||||||
|
tx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trueBool := true
|
||||||
|
adminUser = models.User{
|
||||||
|
Email: "admin@gauth.local",
|
||||||
|
UserName: "admin",
|
||||||
|
Password: string(hashedPassword),
|
||||||
|
EmailVerified: &trueBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&adminUser).Error; err != nil {
|
||||||
|
log.Printf("Failed to create admin user: %v", err)
|
||||||
|
tx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log created admin ID and type for debugging
|
||||||
|
log.Printf("Admin created - ID value: %v (type: %T)", adminUser.ID, adminUser.ID)
|
||||||
|
|
||||||
|
log.Println("✅ Default admin user created:")
|
||||||
|
log.Println(" Email: admin@gauth.local")
|
||||||
|
log.Println(" Password: Admin@123")
|
||||||
|
log.Println(" ⚠️ Please change this password after first login!")
|
||||||
|
} else {
|
||||||
|
// Admin user exists (possibly soft-deleted)
|
||||||
|
if adminUser.DeletedAt.Valid {
|
||||||
|
log.Println("Restoring deleted admin user...")
|
||||||
|
if err := tx.Model(&adminUser).Unscoped().Update("deleted_at", nil).Error; err != nil {
|
||||||
|
log.Printf("Failed to restore admin user: %v", err)
|
||||||
|
tx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log existing admin ID for debugging
|
||||||
|
log.Printf("Admin already exists - ID value: %v (type: %T)", adminUser.ID, adminUser.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure admin role is assigned
|
||||||
|
var adminRole models.Role
|
||||||
|
if err := tx.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
|
||||||
|
log.Printf("Admin role not found: %v", err)
|
||||||
|
tx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(&adminUser).Association("Roles").Append(&adminRole); err != nil {
|
||||||
|
log.Printf("Failed to assign admin role: %v", err)
|
||||||
|
tx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
log.Printf("Failed to commit admin seed transaction: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Varsayılan Yönetici Yaratıldı...")
|
||||||
|
}
|
||||||
26
database/migrate.go
Normal file
26
database/migrate.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Migrate runs AutoMigrate for all models used in the project.
|
||||||
|
func Migrate(db *gorm.DB) error {
|
||||||
|
// Order can matter due to foreign keys; migrate parents first
|
||||||
|
return db.AutoMigrate(
|
||||||
|
&models.User{},
|
||||||
|
&models.SocialAccount{},
|
||||||
|
&models.Role{},
|
||||||
|
&models.Permission{},
|
||||||
|
&models.Category{},
|
||||||
|
&models.Tag{},
|
||||||
|
&models.Post{},
|
||||||
|
&models.CategoryView{},
|
||||||
|
&models.Comment{},
|
||||||
|
&models.CorsWhitelist{},
|
||||||
|
&models.CorsBlacklist{},
|
||||||
|
&models.RateLimitSetting{},
|
||||||
|
)
|
||||||
|
}
|
||||||
274
database/models/blog.go
Normal file
274
database/models/blog.go
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Note: This file maps Django models to GORM models for MySQL.
|
||||||
|
// Image fields are stored as file path strings. Thumbnail generation and image processing
|
||||||
|
// should be handled elsewhere (e.g., during upload) — TODO: integrate with image processing service.
|
||||||
|
|
||||||
|
// Category represents post categories.
|
||||||
|
type Category struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
Title string `gorm:"size:254;not null" json:"title"`
|
||||||
|
Keywords string `gorm:"size:254" json:"keywords"`
|
||||||
|
Desc string `gorm:"size:254" json:"description"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||||
|
Order int `gorm:"default:1;index" json:"order"`
|
||||||
|
Slug string `gorm:"size:250;not null;index" json:"slug"`
|
||||||
|
ParentID *uint64 `gorm:"type:bigint unsigned;index" json:"parent_id"`
|
||||||
|
Parent *Category `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||||
|
Children []*Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||||
|
Image string `gorm:"size:1024" json:"image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Category) TableName() string {
|
||||||
|
return "categories"
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate hook to set slug
|
||||||
|
func (c *Category) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
if c.Slug == "" {
|
||||||
|
c.Slug, err = generateUniqueSlugForCategory(tx, c.Title)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate hook ensures slug exists
|
||||||
|
func (c *Category) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||||
|
if c.Slug == "" {
|
||||||
|
c.Slug, err = generateUniqueSlugForCategory(tx, c.Title)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateUniqueSlugForCategory(db *gorm.DB, title string) (string, error) {
|
||||||
|
slug := normalizeSlug(title)
|
||||||
|
base := slug
|
||||||
|
var count int64
|
||||||
|
try := 1
|
||||||
|
for {
|
||||||
|
db.Model(&Category{}).Where("slug = ?", slug).Count(&count)
|
||||||
|
if count == 0 {
|
||||||
|
return slug, nil
|
||||||
|
}
|
||||||
|
slug = fmt.Sprintf("%s-%d", base, try)
|
||||||
|
try++
|
||||||
|
if try > 1000 {
|
||||||
|
return "", errors.New("unable to generate unique slug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags model
|
||||||
|
type Tag struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
Tag string `gorm:"size:254;not null" json:"tag"`
|
||||||
|
Slug string `gorm:"size:250;not null;index" json:"slug"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Tag) TableName() string { return "tags" }
|
||||||
|
|
||||||
|
func (t *Tag) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
if t.Slug == "" {
|
||||||
|
t.Slug, err = generateUniqueSlugForTag(tx, t.Tag)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tag) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||||
|
if t.Slug == "" {
|
||||||
|
t.Slug, err = generateUniqueSlugForTag(tx, t.Tag)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateUniqueSlugForTag(db *gorm.DB, tag string) (string, error) {
|
||||||
|
slug := normalizeSlug(tag)
|
||||||
|
base := slug
|
||||||
|
var count int64
|
||||||
|
try := 1
|
||||||
|
for {
|
||||||
|
db.Model(&Tag{}).Where("slug = ?", slug).Count(&count)
|
||||||
|
if count == 0 {
|
||||||
|
return slug, nil
|
||||||
|
}
|
||||||
|
slug = fmt.Sprintf("%s-%d", base, try)
|
||||||
|
try++
|
||||||
|
if try > 1000 {
|
||||||
|
return "", errors.New("unable to generate unique slug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post model
|
||||||
|
type Post struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
Title string `gorm:"size:254;not null" json:"title"`
|
||||||
|
UserID *uint64 `gorm:"type:bigint unsigned;index" json:"user_id"`
|
||||||
|
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
Content string `gorm:"type:text" json:"content"`
|
||||||
|
Categories []*Category `gorm:"many2many:post_categories;" json:"categories"`
|
||||||
|
Keywords string `gorm:"size:254" json:"keywords"`
|
||||||
|
Tags []*Tag `gorm:"many2many:post_tags;" json:"tags"`
|
||||||
|
Image string `gorm:"size:1024" json:"image"`
|
||||||
|
Thumb string `gorm:"size:1024" json:"thumb"`
|
||||||
|
Video string `gorm:"size:254;default:'none'" json:"video"`
|
||||||
|
Slug string `gorm:"size:250;not null;index" json:"slug"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||||
|
IsFront bool `gorm:"default:true;index" json:"is_front"`
|
||||||
|
ParentID *uint64 `gorm:"type:bigint unsigned;index" json:"parent_id"`
|
||||||
|
Parent *Post `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||||
|
Children []*Post `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Post) TableName() string { return "posts" }
|
||||||
|
|
||||||
|
func (p *Post) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
if p.Slug == "" {
|
||||||
|
p.Slug, err = generateUniqueSlugForPost(tx, p.Title)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Note: Thumbnail generation should be handled in the upload flow.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Post) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||||
|
if p.Slug == "" {
|
||||||
|
p.Slug, err = generateUniqueSlugForPost(tx, p.Title)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateUniqueSlugForPost(db *gorm.DB, title string) (string, error) {
|
||||||
|
slug := normalizeSlug(title)
|
||||||
|
base := slug
|
||||||
|
var count int64
|
||||||
|
try := 1
|
||||||
|
for {
|
||||||
|
db.Model(&Post{}).Where("slug = ?", slug).Count(&count)
|
||||||
|
if count == 0 {
|
||||||
|
return slug, nil
|
||||||
|
}
|
||||||
|
slug = fmt.Sprintf("%s-%d", base, try)
|
||||||
|
try++
|
||||||
|
if try > 1000 {
|
||||||
|
return "", errors.New("unable to generate unique slug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryView model
|
||||||
|
type CategoryView struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
CategoryID uint64 `gorm:"type:bigint unsigned;index" json:"category_id"`
|
||||||
|
Category *Category `gorm:"foreignKey:CategoryID" json:"category"`
|
||||||
|
IPAddress string `gorm:"size:45;index" json:"ip_address"`
|
||||||
|
UserAgent string `gorm:"type:text" json:"user_agent"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CategoryView) TableName() string { return "category_views" }
|
||||||
|
|
||||||
|
// Comment model
|
||||||
|
type Comment struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
UserID uint64 `gorm:"type:bigint unsigned;index" json:"user_id"`
|
||||||
|
ProductID uint64 `gorm:"type:bigint unsigned;index" json:"product_id"`
|
||||||
|
Product Post `gorm:"foreignKey:ProductID" json:"product"`
|
||||||
|
Title string `gorm:"size:254" json:"title"`
|
||||||
|
Body string `gorm:"type:text" json:"body"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||||
|
Slug string `gorm:"size:250;index" json:"slug"`
|
||||||
|
ParentID *uint64 `gorm:"type:bigint unsigned;index" json:"parent_id"`
|
||||||
|
Parent *Comment `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||||
|
Children []*Comment `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Comment) TableName() string { return "comments" }
|
||||||
|
|
||||||
|
func (c *Comment) BeforeCreate(tx *gorm.DB) (err error) {
|
||||||
|
if c.Slug == "" {
|
||||||
|
c.Slug, err = generateUniqueSlugForComment(tx, c.Title)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Comment) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||||
|
if c.Slug == "" {
|
||||||
|
c.Slug, err = generateUniqueSlugForComment(tx, c.Title)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateUniqueSlugForComment(db *gorm.DB, title string) (string, error) {
|
||||||
|
slug := normalizeSlug(title)
|
||||||
|
base := slug
|
||||||
|
var count int64
|
||||||
|
try := 1
|
||||||
|
for {
|
||||||
|
db.Model(&Comment{}).Where("slug = ?", slug).Count(&count)
|
||||||
|
if count == 0 {
|
||||||
|
return slug, nil
|
||||||
|
}
|
||||||
|
slug = fmt.Sprintf("%s-%d", base, try)
|
||||||
|
try++
|
||||||
|
if try > 1000 {
|
||||||
|
return "", errors.New("unable to generate unique slug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeSlug replaces Turkish characters, lowercases and makes a basic slug.
|
||||||
|
func normalizeSlug(s string) string {
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
"ı", "i",
|
||||||
|
"İ", "i",
|
||||||
|
"ç", "c",
|
||||||
|
"Ç", "c",
|
||||||
|
"ş", "s",
|
||||||
|
"Ş", "s",
|
||||||
|
"ö", "o",
|
||||||
|
"Ö", "o",
|
||||||
|
"ü", "u",
|
||||||
|
"Ü", "u",
|
||||||
|
" ", "-",
|
||||||
|
)
|
||||||
|
s = replacer.Replace(s)
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
// remove multiple dashes
|
||||||
|
for strings.Contains(s, "--") {
|
||||||
|
s = strings.ReplaceAll(s, "--", "-")
|
||||||
|
}
|
||||||
|
// remove extension-like parts
|
||||||
|
s = strings.Trim(s, "-._")
|
||||||
|
// sanitize file-like chars
|
||||||
|
s = filepath.Clean(s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
40
database/models/cors_setting.go
Normal file
40
database/models/cors_setting.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CorsWhitelist - CORS için izin verilen origin'ler
|
||||||
|
type CorsWhitelist struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
|
||||||
|
Description string `gorm:"type:text" json:"description"`
|
||||||
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
|
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CorsBlacklist - CORS için yasaklanan origin'ler
|
||||||
|
type CorsBlacklist struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
Origin string `gorm:"type:varchar(255);uniqueIndex;not null" json:"origin"`
|
||||||
|
Reason string `gorm:"type:text" json:"reason"`
|
||||||
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
|
CreatedBy string `gorm:"type:varchar(255)" json:"created_by,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitSetting - Rate limit ayarları
|
||||||
|
type RateLimitSetting struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
Name string `gorm:"type:varchar(100);uniqueIndex;not null" json:"name"` // e.g., "login", "register", "api"
|
||||||
|
Description string `gorm:"type:text" 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"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
14
database/models/role.go
Normal file
14
database/models/role.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
Name string `gorm:"uniqueIndex;not null" json:"name"` // admin, user
|
||||||
|
Description string `json:"description"`
|
||||||
|
Permissions []Permission `gorm:"many2many:role_permissions;" json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Permission struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
Name string `gorm:"uniqueIndex;not null" json:"name"` // user:read, user:write
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
51
database/models/user.go
Normal file
51
database/models/user.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User model structure
|
||||||
|
type User struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
UserName string `json:"username"`
|
||||||
|
Email string `gorm:"uniqueIndex;not null" json:"email"`
|
||||||
|
Password string `json:"-"` // Password shouldn't be returned in JSON
|
||||||
|
Avatar string `gorm:"type:varchar(500)" json:"avatar,omitempty"` // Avatar URL from OAuth or uploaded
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
|
// Email verification: only required for email/password registration; OAuth users are treated as verified
|
||||||
|
// Changed to *bool to handle false values correctly with GORM defaults
|
||||||
|
EmailVerified *bool `gorm:"default:false" json:"email_verified"` // default false for email/password registration
|
||||||
|
EmailVerifyToken string `gorm:"index" json:"-"`
|
||||||
|
EmailVerifiedAt *time.Time `json:"email_verified_at,omitempty"`
|
||||||
|
|
||||||
|
SocialAccounts []SocialAccount `gorm:"foreignKey:UserID" json:"social_accounts,omitempty"`
|
||||||
|
Roles []Role `gorm:"many2many:user_roles;" json:"roles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to safely get EmailVerified status
|
||||||
|
func (u *User) IsEmailVerified() bool {
|
||||||
|
if u.EmailVerified == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return *u.EmailVerified
|
||||||
|
}
|
||||||
|
|
||||||
|
// SocialAccount model structure
|
||||||
|
type SocialAccount struct {
|
||||||
|
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||||
|
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"`
|
||||||
|
Name string `json:"name,omitempty"` // Full name from provider
|
||||||
|
AvatarURL string `json:"avatar_url,omitempty"` // Avatar URL from provider
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooks can be added here if needed
|
||||||
106
database/redis.go
Normal file
106
database/redis.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gobeyhan/config"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
var RedisClient *redis.Client
|
||||||
|
var ctx = context.Background()
|
||||||
|
|
||||||
|
func ConnectRedis() {
|
||||||
|
redisURL := config.AppConfig.RedisUrl
|
||||||
|
if redisURL == "" {
|
||||||
|
log.Println("Warning: REDIS_URL is not set, continuing without Redis cache")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opt, err := redis.ParseURL(redisURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Failed to parse Redis URL: %v, continuing without Redis cache", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RedisClient = redis.NewClient(opt)
|
||||||
|
|
||||||
|
// Test connection
|
||||||
|
_, err = RedisClient.Ping(ctx).Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Failed to connect to Redis: %v, continuing without Redis cache", err)
|
||||||
|
RedisClient = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Connected to Redis successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set stores a key-value pair in Redis with expiration
|
||||||
|
func Set(key string, value interface{}, expiration time.Duration) error {
|
||||||
|
if RedisClient == nil {
|
||||||
|
return nil // Gracefully handle when Redis is not available
|
||||||
|
}
|
||||||
|
return RedisClient.Set(ctx, key, value, expiration).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a value from Redis
|
||||||
|
func Get(key string) (string, error) {
|
||||||
|
if RedisClient == nil {
|
||||||
|
return "", redis.Nil // Return Nil error when Redis is not available
|
||||||
|
}
|
||||||
|
return RedisClient.Get(ctx, key).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a key from Redis
|
||||||
|
func Delete(key string) error {
|
||||||
|
if RedisClient == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return RedisClient.Del(ctx, key).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists checks if a key exists in Redis
|
||||||
|
func Exists(key string) (bool, error) {
|
||||||
|
if RedisClient == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
count, err := RedisClient.Exists(ctx, key).Result()
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWithJSON stores a JSON-serializable value in Redis
|
||||||
|
func SetEx(key string, value interface{}, seconds int) error {
|
||||||
|
if RedisClient == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return RedisClient.Set(ctx, key, value, time.Duration(seconds)*time.Second).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment increments a counter in Redis
|
||||||
|
func Increment(key string) (int64, error) {
|
||||||
|
if RedisClient == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return RedisClient.Incr(ctx, key).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire sets expiration time for a key
|
||||||
|
func Expire(key string, expiration time.Duration) error {
|
||||||
|
if RedisClient == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return RedisClient.Expire(ctx, key, expiration).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlushAll clears all keys in the current database
|
||||||
|
func FlushAll() error {
|
||||||
|
if RedisClient == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Println("🧹 Clearing Redis Cache...")
|
||||||
|
return RedisClient.FlushDB(ctx).Err()
|
||||||
|
}
|
||||||
3034
docs/docs.go
Normal file
3034
docs/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
3010
docs/swagger.json
Normal file
3010
docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
1942
docs/swagger.yaml
Normal file
1942
docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
78
go.mod
Normal file
78
go.mod
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
module gobeyhan
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/a-h/templ v0.3.977
|
||||||
|
github.com/chai2010/webp v1.4.0
|
||||||
|
github.com/disintegration/imaging v1.6.2
|
||||||
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/redis/go-redis/v9 v9.17.3
|
||||||
|
github.com/swaggo/files v1.0.1
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1
|
||||||
|
github.com/swaggo/swag v1.16.6
|
||||||
|
golang.org/x/crypto v0.48.0
|
||||||
|
golang.org/x/oauth2 v0.35.0
|
||||||
|
gorm.io/driver/mysql v1.6.0
|
||||||
|
gorm.io/gorm v1.31.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||||
|
github.com/go-openapi/spec v0.22.3 // indirect
|
||||||
|
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||||
|
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||||
|
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||||
|
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/arch v0.24.0 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
||||||
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
|
golang.org/x/net v0.50.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
215
go.sum
Normal file
215
go.sum
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||||
|
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
|
||||||
|
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||||
|
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/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
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/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
|
||||||
|
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
|
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/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
|
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
|
||||||
|
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
|
||||||
|
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
|
||||||
|
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||||
|
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||||
|
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
||||||
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
||||||
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
||||||
|
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
||||||
|
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
||||||
|
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
||||||
|
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||||
|
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
||||||
|
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
|
||||||
|
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
||||||
|
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||||
|
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||||
|
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
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.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
|
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
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-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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
|
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
||||||
|
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||||
|
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
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/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
||||||
|
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
|
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||||
|
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
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/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
537
internal/handler/admin/blog_handler.go
Normal file
537
internal/handler/admin/blog_handler.go
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/config"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"gobeyhan/pkg/utils"
|
||||||
|
"gobeyhan/views/admin/blog"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BlogHandler struct{}
|
||||||
|
|
||||||
|
func NewBlogHandler() *BlogHandler {
|
||||||
|
return &BlogHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List displays all blog posts
|
||||||
|
func (h *BlogHandler) List(c *gin.Context) {
|
||||||
|
var posts []models.Post
|
||||||
|
err := database.DB.
|
||||||
|
Preload("User").
|
||||||
|
Preload("Categories").
|
||||||
|
Preload("Tags").
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&posts).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[BlogHandler.List] Error fetching posts: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error fetching posts")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
blog.List(posts).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New displays the create form
|
||||||
|
func (h *BlogHandler) New(c *gin.Context) {
|
||||||
|
var categories []models.Category
|
||||||
|
var tags []models.Tag
|
||||||
|
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&categories)
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&tags)
|
||||||
|
|
||||||
|
blog.Create(categories, tags, nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create handles post creation
|
||||||
|
func (h *BlogHandler) Create(c *gin.Context) {
|
||||||
|
title := c.PostForm("title")
|
||||||
|
content := c.PostForm("content")
|
||||||
|
keywords := c.PostForm("keywords")
|
||||||
|
isActive := c.PostForm("is_active") == "on"
|
||||||
|
isFront := c.PostForm("is_front") == "on"
|
||||||
|
|
||||||
|
log.Printf("[BlogHandler.Create] Received: Title=%s, ContentSize=%d, Active=%v", title, len(content), isActive)
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
errors := make(map[string]string)
|
||||||
|
if title == "" {
|
||||||
|
errors["title"] = "Title is required"
|
||||||
|
}
|
||||||
|
if content == "" {
|
||||||
|
errors["content"] = "Content is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
log.Printf("[BlogHandler.Create] Validation failed: %v", errors)
|
||||||
|
var categories []models.Category
|
||||||
|
var tags []models.Tag
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&categories)
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&tags)
|
||||||
|
blog.Create(categories, tags, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle image upload
|
||||||
|
imagePath := c.PostForm("image") // Fallback to manual link if provided
|
||||||
|
file, err := c.FormFile("image_file")
|
||||||
|
if err == nil && file != nil {
|
||||||
|
opts := &utils.ImageOptions{
|
||||||
|
Width: config.AppConfig.PostImageWidth,
|
||||||
|
Height: config.AppConfig.PostImageHeight,
|
||||||
|
Quality: float32(config.AppConfig.PostImageQuality),
|
||||||
|
Format: config.AppConfig.PostImageFormat,
|
||||||
|
Mode: config.AppConfig.PostImageMode,
|
||||||
|
}
|
||||||
|
path, err := utils.SaveOptimizedImage(file, filepath.Join("uploads", "blog"), "post", opts)
|
||||||
|
if err == nil {
|
||||||
|
imagePath = path
|
||||||
|
log.Printf("[BlogHandler.Create] Image saved to: %s", imagePath)
|
||||||
|
} else {
|
||||||
|
log.Printf("[BlogHandler.Create] Image upload error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create post
|
||||||
|
post := models.Post{
|
||||||
|
Title: title,
|
||||||
|
Content: content,
|
||||||
|
Keywords: keywords,
|
||||||
|
Image: imagePath,
|
||||||
|
IsActive: isActive,
|
||||||
|
IsFront: isFront,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Create(&post).Error; err != nil {
|
||||||
|
log.Printf("[BlogHandler.Create] DB Create error: %v", err)
|
||||||
|
errors["general"] = "Error creating post: " + err.Error()
|
||||||
|
var categories []models.Category
|
||||||
|
var tags []models.Tag
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&categories)
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&tags)
|
||||||
|
blog.Create(categories, tags, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[BlogHandler.Create] Post created successfully: ID=%d", post.ID)
|
||||||
|
|
||||||
|
// Handle categories
|
||||||
|
categoryIDs := c.PostFormArray("category_ids")
|
||||||
|
if len(categoryIDs) > 0 {
|
||||||
|
var categories []*models.Category
|
||||||
|
for _, idStr := range categoryIDs {
|
||||||
|
id, _ := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
categories = append(categories, &models.Category{ID: id})
|
||||||
|
}
|
||||||
|
if err := database.DB.Model(&post).Association("Categories").Replace(categories); err != nil {
|
||||||
|
log.Printf("[BlogHandler.Create] Error associating categories: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tags
|
||||||
|
tagIDs := c.PostFormArray("tag_ids")
|
||||||
|
if len(tagIDs) > 0 {
|
||||||
|
var tags []*models.Tag
|
||||||
|
for _, idStr := range tagIDs {
|
||||||
|
id, _ := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
tags = append(tags, &models.Tag{ID: id})
|
||||||
|
}
|
||||||
|
if err := database.DB.Model(&post).Association("Tags").Replace(tags); err != nil {
|
||||||
|
log.Printf("[BlogHandler.Create] Error associating tags: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit displays the edit form
|
||||||
|
func (h *BlogHandler) Edit(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var post models.Post
|
||||||
|
err = database.DB.
|
||||||
|
Preload("Categories").
|
||||||
|
Preload("Tags").
|
||||||
|
First(&post, id).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[BlogHandler.Edit] Post not found: ID=%d, error=%v", id, err)
|
||||||
|
c.String(http.StatusNotFound, "Post not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var categories []models.Category
|
||||||
|
var tags []models.Tag
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&categories)
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&tags)
|
||||||
|
|
||||||
|
blog.Edit(&post, categories, tags, nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update handles post updates
|
||||||
|
func (h *BlogHandler) Update(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
idUint, _ := strconv.ParseUint(id, 10, 64)
|
||||||
|
|
||||||
|
title := c.PostForm("title")
|
||||||
|
content := c.PostForm("content")
|
||||||
|
keywords := c.PostForm("keywords")
|
||||||
|
isActive := c.PostForm("is_active") == "on"
|
||||||
|
isFront := c.PostForm("is_front") == "on"
|
||||||
|
|
||||||
|
log.Printf("[BlogHandler.Update] Received update for ID=%s: Title=%s, ContentSize=%d", id, title, len(content))
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
errors := make(map[string]string)
|
||||||
|
if title == "" {
|
||||||
|
errors["title"] = "Title is required"
|
||||||
|
}
|
||||||
|
if content == "" {
|
||||||
|
errors["content"] = "Content is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
log.Printf("[BlogHandler.Update] Validation failed: %v", errors)
|
||||||
|
var post models.Post
|
||||||
|
database.DB.Preload("Categories").Preload("Tags").First(&post, idUint)
|
||||||
|
var categories []models.Category
|
||||||
|
var tags []models.Tag
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&categories)
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&tags)
|
||||||
|
blog.Edit(&post, categories, tags, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle image upload
|
||||||
|
imagePath := c.PostForm("image") // Keep existing or use manual link
|
||||||
|
file, err := c.FormFile("image_file")
|
||||||
|
if err == nil && file != nil {
|
||||||
|
opts := &utils.ImageOptions{
|
||||||
|
Width: config.AppConfig.PostImageWidth,
|
||||||
|
Height: config.AppConfig.PostImageHeight,
|
||||||
|
Quality: float32(config.AppConfig.PostImageQuality),
|
||||||
|
Format: config.AppConfig.PostImageFormat,
|
||||||
|
Mode: config.AppConfig.PostImageMode,
|
||||||
|
}
|
||||||
|
path, err := utils.SaveOptimizedImage(file, filepath.Join("uploads", "blog"), "post", opts)
|
||||||
|
if err == nil {
|
||||||
|
imagePath = path
|
||||||
|
log.Printf("[BlogHandler.Update] Image updated to: %s", imagePath)
|
||||||
|
} else {
|
||||||
|
log.Printf("[BlogHandler.Update] Image upload error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update post
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"title": title,
|
||||||
|
"content": content,
|
||||||
|
"keywords": keywords,
|
||||||
|
"image": imagePath,
|
||||||
|
"is_active": isActive,
|
||||||
|
"is_front": isFront,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Model(&models.Post{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||||
|
log.Printf("[BlogHandler.Update] DB Update error: %v", err)
|
||||||
|
errors["general"] = "Error updating post"
|
||||||
|
var post models.Post
|
||||||
|
database.DB.Preload("Categories").Preload("Tags").First(&post, idUint)
|
||||||
|
var categories []models.Category
|
||||||
|
var tags []models.Tag
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&categories)
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&tags)
|
||||||
|
blog.Edit(&post, categories, tags, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update categories and tags separately (GORM Associations)
|
||||||
|
var post models.Post
|
||||||
|
if err := database.DB.First(&post, idUint).Error; err == nil {
|
||||||
|
// Categories
|
||||||
|
categoryIDs := c.PostFormArray("category_ids")
|
||||||
|
var categories []*models.Category
|
||||||
|
for _, idStr := range categoryIDs {
|
||||||
|
catID, _ := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
categories = append(categories, &models.Category{ID: catID})
|
||||||
|
}
|
||||||
|
database.DB.Model(&post).Association("Categories").Replace(categories)
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
tagIDs := c.PostFormArray("tag_ids")
|
||||||
|
var tags []*models.Tag
|
||||||
|
for _, idStr := range tagIDs {
|
||||||
|
tagID, _ := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
tags = append(tags, &models.Tag{ID: tagID})
|
||||||
|
}
|
||||||
|
database.DB.Model(&post).Association("Tags").Replace(tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[BlogHandler.Update] Post updated successfully: ID=%s", id)
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete handles post deletion
|
||||||
|
func (h *BlogHandler) Delete(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
log.Printf("[BlogHandler.Delete] Deleting ID=%s", id)
|
||||||
|
|
||||||
|
if err := database.DB.Delete(&models.Post{}, "id = ?", id).Error; err != nil {
|
||||||
|
log.Printf("[BlogHandler.Delete] Error: %v", err)
|
||||||
|
c.String(http.StatusInternalServerError, "Error deleting post")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// CATEGORY HANDLERS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
func (h *BlogHandler) ListCategories(c *gin.Context) {
|
||||||
|
var categories []models.Category
|
||||||
|
database.DB.Order("`order` ASC").Find(&categories)
|
||||||
|
blog.CategoryList(categories).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) NewCategory(c *gin.Context) {
|
||||||
|
var categories []models.Category
|
||||||
|
database.DB.Find(&categories)
|
||||||
|
blog.CategoryForm(&models.Category{}, categories, nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) CreateCategory(c *gin.Context) {
|
||||||
|
var cat models.Category
|
||||||
|
cat.Title = c.PostForm("title")
|
||||||
|
cat.Slug = c.PostForm("slug")
|
||||||
|
cat.Desc = c.PostForm("description")
|
||||||
|
cat.Keywords = c.PostForm("keywords")
|
||||||
|
cat.IsActive = c.PostForm("is_active") == "on"
|
||||||
|
|
||||||
|
order, _ := strconv.Atoi(c.PostForm("order"))
|
||||||
|
cat.Order = order
|
||||||
|
|
||||||
|
parentIDStr := c.PostForm("parent_id")
|
||||||
|
if parentIDStr != "" {
|
||||||
|
pID, _ := strconv.ParseUint(parentIDStr, 10, 64)
|
||||||
|
cat.ParentID = &pID
|
||||||
|
}
|
||||||
|
|
||||||
|
if cat.Title == "" {
|
||||||
|
errors := map[string]string{"title": "Title is required"}
|
||||||
|
var categories []models.Category
|
||||||
|
database.DB.Find(&categories)
|
||||||
|
blog.CategoryForm(&cat, categories, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Create(&cat).Error; err != nil {
|
||||||
|
errors := map[string]string{"general": err.Error()}
|
||||||
|
var categories []models.Category
|
||||||
|
database.DB.Find(&categories)
|
||||||
|
blog.CategoryForm(&cat, categories, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog/categories")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) EditCategory(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var cat models.Category
|
||||||
|
if err := database.DB.First(&cat, id).Error; err != nil {
|
||||||
|
c.String(http.StatusNotFound, "Category not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var categories []models.Category
|
||||||
|
database.DB.Find(&categories)
|
||||||
|
blog.CategoryForm(&cat, categories, nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) UpdateCategory(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var cat models.Category
|
||||||
|
if err := database.DB.First(&cat, id).Error; err != nil {
|
||||||
|
c.String(http.StatusNotFound, "Category not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cat.Title = c.PostForm("title")
|
||||||
|
cat.Slug = c.PostForm("slug")
|
||||||
|
cat.Desc = c.PostForm("description")
|
||||||
|
cat.Keywords = c.PostForm("keywords")
|
||||||
|
cat.IsActive = c.PostForm("is_active") == "on"
|
||||||
|
|
||||||
|
order, _ := strconv.Atoi(c.PostForm("order"))
|
||||||
|
cat.Order = order
|
||||||
|
|
||||||
|
parentIDStr := c.PostForm("parent_id")
|
||||||
|
if parentIDStr != "" {
|
||||||
|
pID, _ := strconv.ParseUint(parentIDStr, 10, 64)
|
||||||
|
cat.ParentID = &pID
|
||||||
|
} else {
|
||||||
|
cat.ParentID = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cat.Title == "" {
|
||||||
|
errors := map[string]string{"title": "Title is required"}
|
||||||
|
var categories []models.Category
|
||||||
|
database.DB.Find(&categories)
|
||||||
|
blog.CategoryForm(&cat, categories, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Save(&cat).Error; err != nil {
|
||||||
|
errors := map[string]string{"general": err.Error()}
|
||||||
|
var categories []models.Category
|
||||||
|
database.DB.Find(&categories)
|
||||||
|
blog.CategoryForm(&cat, categories, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog/categories")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) DeleteCategory(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
database.DB.Delete(&models.Category{}, id)
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog/categories")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TAG HANDLERS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
func (h *BlogHandler) ListTags(c *gin.Context) {
|
||||||
|
var tags []models.Tag
|
||||||
|
database.DB.Order("tag ASC").Find(&tags)
|
||||||
|
blog.TagList(tags).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) NewTag(c *gin.Context) {
|
||||||
|
blog.TagForm(&models.Tag{}, nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) CreateTag(c *gin.Context) {
|
||||||
|
var tag models.Tag
|
||||||
|
tag.Tag = c.PostForm("tag")
|
||||||
|
tag.Slug = c.PostForm("slug")
|
||||||
|
tag.IsActive = true // Default
|
||||||
|
|
||||||
|
if tag.Tag == "" {
|
||||||
|
errors := map[string]string{"tag": "Tag name is required"}
|
||||||
|
blog.TagForm(&tag, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Create(&tag).Error; err != nil {
|
||||||
|
errors := map[string]string{"general": err.Error()}
|
||||||
|
blog.TagForm(&tag, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog/tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) EditTag(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var tag models.Tag
|
||||||
|
if err := database.DB.First(&tag, id).Error; err != nil {
|
||||||
|
c.String(http.StatusNotFound, "Tag not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
blog.TagForm(&tag, nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) UpdateTag(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var tag models.Tag
|
||||||
|
if err := database.DB.First(&tag, id).Error; err != nil {
|
||||||
|
c.String(http.StatusNotFound, "Tag not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.Tag = c.PostForm("tag")
|
||||||
|
tag.Slug = c.PostForm("slug")
|
||||||
|
|
||||||
|
if tag.Tag == "" {
|
||||||
|
errors := map[string]string{"tag": "Tag name is required"}
|
||||||
|
blog.TagForm(&tag, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Save(&tag).Error; err != nil {
|
||||||
|
errors := map[string]string{"general": err.Error()}
|
||||||
|
blog.TagForm(&tag, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog/tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) DeleteTag(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
database.DB.Delete(&models.Tag{}, id)
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog/tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// COMMENT HANDLERS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
func (h *BlogHandler) ListComments(c *gin.Context) {
|
||||||
|
var comments []models.Comment
|
||||||
|
database.DB.Preload("Product").Order("created_at DESC").Find(&comments)
|
||||||
|
blog.CommentList(comments).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) EditComment(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var comment models.Comment
|
||||||
|
if err := database.DB.Preload("Product").First(&comment, id).Error; err != nil {
|
||||||
|
c.String(http.StatusNotFound, "Comment not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
blog.CommentForm(&comment, nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) UpdateComment(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var comment models.Comment
|
||||||
|
if err := database.DB.First(&comment, id).Error; err != nil {
|
||||||
|
c.String(http.StatusNotFound, "Comment not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comment.Body = c.PostForm("body")
|
||||||
|
comment.IsActive = c.PostForm("is_active") == "on"
|
||||||
|
|
||||||
|
if err := database.DB.Save(&comment).Error; err != nil {
|
||||||
|
errors := map[string]string{"general": err.Error()}
|
||||||
|
blog.CommentForm(&comment, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog/comments")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BlogHandler) DeleteComment(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
database.DB.Delete(&models.Comment{}, id)
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/blog/comments")
|
||||||
|
}
|
||||||
26
internal/handler/admin/handler.go
Normal file
26
internal/handler/admin/handler.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
view "gobeyhan/views/admin"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct{}
|
||||||
|
|
||||||
|
func NewHandler() *Handler {
|
||||||
|
return &Handler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) LoginPage(c *gin.Context) {
|
||||||
|
view.Login().Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) LoginPost(c *gin.Context) {
|
||||||
|
// TODO: Implement actual login logic
|
||||||
|
c.Redirect(303, "/admin/dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Dashboard(c *gin.Context) {
|
||||||
|
view.Dashboard().Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
308
internal/handler/admin/settings_handler.go
Normal file
308
internal/handler/admin/settings_handler.go
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/app/settings/services"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"gobeyhan/views/admin/settings" // We will create this package
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SettingsHandler struct {
|
||||||
|
service *services.SettingsService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettingsHandler() *SettingsHandler {
|
||||||
|
return &SettingsHandler{
|
||||||
|
service: services.NewSettingsService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== WHITELIST ====================
|
||||||
|
|
||||||
|
func (h *SettingsHandler) ListWhitelist(c *gin.Context) {
|
||||||
|
items, err := h.service.GetAllCorsWhitelist()
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Error fetching whitelist")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settings.WhitelistList(items).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) NewWhitelist(c *gin.Context) {
|
||||||
|
settings.WhitelistCreate(nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) CreateWhitelist(c *gin.Context) {
|
||||||
|
origin := c.PostForm("origin")
|
||||||
|
description := c.PostForm("description")
|
||||||
|
|
||||||
|
// Basic Validation
|
||||||
|
errors := make(map[string]string)
|
||||||
|
if origin == "" {
|
||||||
|
errors["origin"] = "Origin is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
settings.WhitelistCreate(errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item := &models.CorsWhitelist{
|
||||||
|
Origin: origin,
|
||||||
|
Description: description,
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreateCorsWhitelist(item); err != nil {
|
||||||
|
errors["origin"] = "Error creating whitelist entry: " + err.Error()
|
||||||
|
settings.WhitelistCreate(errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/settings/whitelist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) EditWhitelist(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := h.service.GetCorsWhitelistByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusNotFound, "Item not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.WhitelistEdit(item, nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) UpdateWhitelist(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
origin := c.PostForm("origin")
|
||||||
|
description := c.PostForm("description")
|
||||||
|
|
||||||
|
// Basic Validation
|
||||||
|
errors := make(map[string]string)
|
||||||
|
if origin == "" {
|
||||||
|
errors["origin"] = "Origin is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
// Fetch item again to display form with errors
|
||||||
|
idUint, _ := strconv.ParseUint(id, 10, 64)
|
||||||
|
item, _ := h.service.GetCorsWhitelistByID(idUint)
|
||||||
|
if item == nil {
|
||||||
|
c.String(http.StatusNotFound, "Item not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Preserve user input
|
||||||
|
item.Origin = origin
|
||||||
|
item.Description = description
|
||||||
|
settings.WhitelistEdit(item, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"origin": origin,
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateCorsWhitelist(id, updates); err != nil {
|
||||||
|
idUint, _ := strconv.ParseUint(id, 10, 64)
|
||||||
|
item, _ := h.service.GetCorsWhitelistByID(idUint)
|
||||||
|
settings.WhitelistEdit(item, map[string]string{"origin": "Error updating: " + err.Error()}).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/settings/whitelist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) DeleteWhitelist(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := h.service.DeleteCorsWhitelist(id); err != nil { // Service takes string ID
|
||||||
|
c.String(http.StatusInternalServerError, "Error deleting item")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/settings/whitelist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== BLACKLIST ====================
|
||||||
|
|
||||||
|
func (h *SettingsHandler) ListBlacklist(c *gin.Context) {
|
||||||
|
items, err := h.service.GetAllCorsBlacklist()
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Error fetching blacklist")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settings.BlacklistList(items).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) NewBlacklist(c *gin.Context) {
|
||||||
|
settings.BlacklistCreate(nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) CreateBlacklist(c *gin.Context) {
|
||||||
|
origin := c.PostForm("origin")
|
||||||
|
description := c.PostForm("description")
|
||||||
|
|
||||||
|
errors := make(map[string]string)
|
||||||
|
if origin == "" {
|
||||||
|
errors["origin"] = "Origin is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
settings.BlacklistCreate(errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item := &models.CorsBlacklist{
|
||||||
|
Origin: origin,
|
||||||
|
Reason: description,
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.CreateCorsBlacklist(item); err != nil {
|
||||||
|
errors["origin"] = "Error creating entry: " + err.Error()
|
||||||
|
settings.BlacklistCreate(errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/settings/blacklist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) DeleteBlacklist(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := h.service.DeleteCorsBlacklist(id); err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Error deleting item")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/settings/blacklist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) EditBlacklist(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := h.service.GetCorsBlacklistByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusNotFound, "Item not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.BlacklistEdit(item, nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) UpdateBlacklist(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
origin := c.PostForm("origin")
|
||||||
|
reason := c.PostForm("reason")
|
||||||
|
|
||||||
|
errors := make(map[string]string)
|
||||||
|
if origin == "" {
|
||||||
|
errors["origin"] = "Origin is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
idUint, _ := strconv.ParseUint(id, 10, 64)
|
||||||
|
item, _ := h.service.GetCorsBlacklistByID(idUint)
|
||||||
|
if item == nil {
|
||||||
|
c.String(http.StatusNotFound, "Item not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.Origin = origin
|
||||||
|
item.Reason = reason
|
||||||
|
settings.BlacklistEdit(item, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"origin": origin,
|
||||||
|
"reason": reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateCorsBlacklist(id, updates); err != nil {
|
||||||
|
idUint, _ := strconv.ParseUint(id, 10, 64)
|
||||||
|
item, _ := h.service.GetCorsBlacklistByID(idUint)
|
||||||
|
settings.BlacklistEdit(item, map[string]string{"origin": "Error updating: " + err.Error()}).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/settings/blacklist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RATE LIMITS ====================
|
||||||
|
|
||||||
|
func (h *SettingsHandler) ListRateLimits(c *gin.Context) {
|
||||||
|
items, err := h.service.GetAllRateLimitSettings()
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Error fetching rate limits")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settings.RateLimitList(items).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) EditRateLimit(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := h.service.GetRateLimitSettingByID(id)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusNotFound, "Item not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.RateLimitEdit(item, nil).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) UpdateRateLimit(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
limitStr := c.PostForm("max_requests")
|
||||||
|
windowStr := c.PostForm("window_seconds")
|
||||||
|
description := c.PostForm("description")
|
||||||
|
|
||||||
|
limit, _ := strconv.ParseInt(limitStr, 10, 64)
|
||||||
|
window, _ := strconv.Atoi(windowStr)
|
||||||
|
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
if limit > 0 {
|
||||||
|
updates["max_requests"] = limit
|
||||||
|
}
|
||||||
|
if window > 0 {
|
||||||
|
updates["window_seconds"] = window
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.UpdateRateLimitSetting(id, updates); err != nil {
|
||||||
|
// Handle error (redisplay form)
|
||||||
|
idUint, _ := strconv.ParseUint(id, 10, 64)
|
||||||
|
item, _ := h.service.GetRateLimitSettingByID(idUint)
|
||||||
|
settings.RateLimitEdit(item, map[string]string{"general": "Error updating: " + err.Error()}).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/settings/rate-limits")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SettingsHandler) DeleteRateLimit(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := h.service.DeleteRateLimitSetting(id); err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Error deleting item")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/settings/rate-limits")
|
||||||
|
}
|
||||||
195
internal/handler/admin/user_handler.go
Normal file
195
internal/handler/admin/user_handler.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gobeyhan/app/account/services"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
view "gobeyhan/views/admin/user"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserHandler struct {
|
||||||
|
userService *services.UserService
|
||||||
|
roleService *services.RoleService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserHandler() *UserHandler {
|
||||||
|
return &UserHandler{
|
||||||
|
userService: services.NewUserService(),
|
||||||
|
roleService: services.NewRoleService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List Users
|
||||||
|
func (h *UserHandler) List(c *gin.Context) {
|
||||||
|
users, _, err := h.userService.GetAllUsers(false, 1, 100) // TODO: Implement pagination
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Error fetching users")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
view.List(users).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New User Form
|
||||||
|
func (h *UserHandler) New(c *gin.Context) {
|
||||||
|
roles, _ := h.roleService.GetAllRoles()
|
||||||
|
view.Create(roles, map[string]string{}).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create User Action
|
||||||
|
func (h *UserHandler) Create(c *gin.Context) {
|
||||||
|
username := c.PostForm("username")
|
||||||
|
email := c.PostForm("email")
|
||||||
|
password := c.PostForm("password")
|
||||||
|
|
||||||
|
// Basic Validation
|
||||||
|
errors := make(map[string]string)
|
||||||
|
if username == "" {
|
||||||
|
errors["username"] = "Username is required"
|
||||||
|
}
|
||||||
|
if email == "" {
|
||||||
|
errors["email"] = "Email is required"
|
||||||
|
}
|
||||||
|
if password == "" {
|
||||||
|
errors["password"] = "Password is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
roles, _ := h.roleService.GetAllRoles()
|
||||||
|
view.Create(roles, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
UserName: username,
|
||||||
|
Email: email,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.userService.CreateUser(user, password); err != nil {
|
||||||
|
errors["email"] = "Error creating user (e.g. email exists)"
|
||||||
|
roles, _ := h.roleService.GetAllRoles()
|
||||||
|
view.Create(roles, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Role Assignment
|
||||||
|
roleIDStr := c.PostForm("role_id")
|
||||||
|
if roleID, err := strconv.ParseUint(roleIDStr, 10, 64); err == nil && roleID > 0 {
|
||||||
|
h.userService.AssignRole(user.ID, roleID)
|
||||||
|
} else {
|
||||||
|
// Assign default role if no role selected (or as fallback)
|
||||||
|
h.userService.AssignDefaultRole(user.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Email Verification
|
||||||
|
emailVerified := c.PostForm("email_verified") == "on"
|
||||||
|
if emailVerified {
|
||||||
|
h.userService.UpdateUser(user.ID, map[string]interface{}{
|
||||||
|
"email_verified": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit User Form
|
||||||
|
func (h *UserHandler) Edit(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.userService.GetUserByID(id)
|
||||||
|
if err != nil || user == nil {
|
||||||
|
c.String(http.StatusNotFound, "User not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, _ := h.roleService.GetAllRoles()
|
||||||
|
view.Edit(*user, roles, map[string]string{}).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update User Action
|
||||||
|
func (h *UserHandler) Update(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
username := c.PostForm("username")
|
||||||
|
email := c.PostForm("email")
|
||||||
|
password := c.PostForm("password")
|
||||||
|
|
||||||
|
// Basic Validation
|
||||||
|
errors := make(map[string]string)
|
||||||
|
if username == "" {
|
||||||
|
errors["username"] = "Username is required"
|
||||||
|
}
|
||||||
|
if email == "" {
|
||||||
|
errors["email"] = "Email is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
user, _ := h.userService.GetUserByID(id)
|
||||||
|
if user != nil {
|
||||||
|
// Keep submitted values? simplified for now
|
||||||
|
user.UserName = username
|
||||||
|
user.Email = email
|
||||||
|
roles, _ := h.roleService.GetAllRoles()
|
||||||
|
view.Edit(*user, roles, errors).Render(c.Request.Context(), c.Writer)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"email": email,
|
||||||
|
"email_verified": c.PostForm("email_verified") == "on",
|
||||||
|
}
|
||||||
|
if password != "" {
|
||||||
|
updates["password"] = password
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.userService.UpdateUser(id, updates); err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Error updating user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Role
|
||||||
|
roleIDStr := c.PostForm("role_id")
|
||||||
|
if roleID, err := strconv.ParseUint(roleIDStr, 10, 64); err == nil && roleID > 0 {
|
||||||
|
// Remove existing roles first (simplified approach for single role)
|
||||||
|
// Ideally we should check if role changed
|
||||||
|
user, _ := h.userService.GetUserByID(id)
|
||||||
|
if len(user.Roles) > 0 {
|
||||||
|
h.userService.RemoveRole(id, user.Roles[0].ID)
|
||||||
|
}
|
||||||
|
h.userService.AssignRole(id, roleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete User Action
|
||||||
|
func (h *UserHandler) Delete(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusBadRequest, "Invalid ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.userService.DeleteUser(id); err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, "Error deleting user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/users")
|
||||||
|
}
|
||||||
170
main.go
Normal file
170
main.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gobeyhan/app/routes"
|
||||||
|
"gobeyhan/config"
|
||||||
|
"gobeyhan/database"
|
||||||
|
"gobeyhan/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
|
||||||
|
_ "gobeyhan/docs" // Swagger docs
|
||||||
|
)
|
||||||
|
|
||||||
|
// @title Beyhan Backend API
|
||||||
|
// @version 2.0
|
||||||
|
// @description Modular REST API with Blog, Account, and Settings apps
|
||||||
|
// @termsOfService http://swagger.io/terms/
|
||||||
|
|
||||||
|
// @contact.name API Support
|
||||||
|
// @contact.email support@beyhan.com
|
||||||
|
|
||||||
|
// @license.name MIT
|
||||||
|
// @license.url https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// @host localhost:8080
|
||||||
|
// @BasePath /
|
||||||
|
|
||||||
|
// @securityDefinitions.apikey BearerAuth
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
// @description Type "Bearer" followed by a space and JWT token.
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
migrateFlag := flag.Bool("migrate", false, "Run database migrations (AutoMigrate)")
|
||||||
|
seedFlag := flag.Bool("seed", false, "Run database seeds (may include creating admin user)")
|
||||||
|
migrateOnlyFlag := flag.Bool("migrate-only", false, "Run migrations and exit (don't start server)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
database.ConnectDB()
|
||||||
|
|
||||||
|
// If seed flag passed, run full seed (includes migrations) immediately regardless of env
|
||||||
|
if *seedFlag {
|
||||||
|
database.SeedAll()
|
||||||
|
log.Println("Seeding complete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine environment: prefer config.AppConfig.Env (set by LoadConfig), else fallback to APP_ENV env var
|
||||||
|
env := "development"
|
||||||
|
if config.AppConfig != nil && config.AppConfig.Env != "" {
|
||||||
|
env = config.AppConfig.Env
|
||||||
|
} else if v := os.Getenv("APP_ENV"); v != "" {
|
||||||
|
env = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration handling
|
||||||
|
shouldMigrate := *migrateFlag || *migrateOnlyFlag || env == "development"
|
||||||
|
if shouldMigrate {
|
||||||
|
if err := database.Migrate(database.DB); err != nil {
|
||||||
|
log.Fatalf("Migration Yapılamadı: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("Migration complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If migrate-only flag, exit after migration
|
||||||
|
if *migrateOnlyFlag {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
forceSeed := os.Getenv("FORCE_SEED")
|
||||||
|
if strings.ToLower(forceSeed) == "true" {
|
||||||
|
database.SeedAll()
|
||||||
|
log.Println("Seeding complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup Redis
|
||||||
|
database.ConnectRedis()
|
||||||
|
if err := database.FlushAll(); err != nil {
|
||||||
|
log.Printf("Warning: Failed to flush Redis cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print banner
|
||||||
|
fmt.Println(`
|
||||||
|
╔════════════════════════════════════════════════════════╗
|
||||||
|
║ ║
|
||||||
|
║ ██████╗ ███████╗██╗ ██╗██╗ ██╗ █████╗ ███╗ ██╗ ║
|
||||||
|
║ ██╔══██╗██╔════╝╚██╗ ██╔╝██║ ██║██╔══██╗████╗ ██║ ║
|
||||||
|
║ ██████╔╝█████╗ ╚████╔╝ ███████║███████║██╔██╗ ██║ ║
|
||||||
|
║ ██╔══██╗██╔══╝ ╚██╔╝ ██╔══██║██╔══██║██║╚██╗██║ ║
|
||||||
|
║ ██████╔╝███████╗ ██║ ██║ ██║██║ ██║██║ ╚████║ ║
|
||||||
|
║ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ║
|
||||||
|
║ ║
|
||||||
|
║ ██████╗ █████╗ ██████╗██╗ ██╗ ║
|
||||||
|
║ ██╔══██╗██╔══██╗██╔════╝██║ ██╔╝ ║
|
||||||
|
║ ██████╔╝███████║██║ █████╔╝ ║
|
||||||
|
║ ██╔══██╗██╔══██║██║ ██╔═██╗ ║
|
||||||
|
║ ██████╔╝██║ ██║╚██████╗██║ ██╗ ║
|
||||||
|
║ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ║
|
||||||
|
║ BACKEND ║
|
||||||
|
║ ║
|
||||||
|
╚════════════════════════════════════════════════════════╝
|
||||||
|
`)
|
||||||
|
fmt.Println(" Go Backend | v2.0.0 | " + utils.ColorGreen + "Running" + utils.ColorReset)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Initialize Gin
|
||||||
|
if env == "production" {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// Disable automatic redirects to prevent infinite loops with SPA routing
|
||||||
|
r.RedirectTrailingSlash = false
|
||||||
|
r.RedirectFixedPath = false
|
||||||
|
|
||||||
|
// Swagger endpoint
|
||||||
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
|
||||||
|
// Setup routes from modular apps
|
||||||
|
routes.SetupRoutes(r)
|
||||||
|
|
||||||
|
// Register Web Handlers (Frontend / Admin Panel)
|
||||||
|
// admin_panel.RegisterHandlers(r)
|
||||||
|
|
||||||
|
trustedEnv := strings.TrimSpace(os.Getenv("TRUSTED_PROXIES"))
|
||||||
|
var trusted []string
|
||||||
|
if trustedEnv != "" {
|
||||||
|
for _, s := range strings.Split(trustedEnv, ",") {
|
||||||
|
if t := strings.TrimSpace(s); t != "" {
|
||||||
|
trusted = append(trusted, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Nil ile çağırmak, "hiçbir proxy'ye güvenme" davranışını sağlar
|
||||||
|
trusted = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set trusted proxies on Gin router to avoid the "trusted all proxies" warning.
|
||||||
|
// If `trusted` is nil, no proxies will be trusted. This will error if the list contains an invalid entry.
|
||||||
|
if err := r.SetTrustedProxies(trusted); err != nil {
|
||||||
|
log.Fatalf("Failed to set trusted proxies: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Trusted proxies configured: %v", trusted)
|
||||||
|
|
||||||
|
// Get port from config
|
||||||
|
port := config.AppConfig.Port
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
log.Printf("🚀 Server starting on port %s", port)
|
||||||
|
log.Printf("📚 Swagger UI: http://localhost:%s/swagger/index.html", port)
|
||||||
|
log.Printf("🌐 API Base: http://localhost:%s/api/v1", port)
|
||||||
|
|
||||||
|
if err := r.Run(":" + port); err != nil {
|
||||||
|
log.Fatalf("Failed to start server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
pkg/utils/colors.go
Normal file
12
pkg/utils/colors.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
const (
|
||||||
|
ColorReset = "\033[0m"
|
||||||
|
ColorRed = "\033[31m"
|
||||||
|
ColorGreen = "\033[32m"
|
||||||
|
ColorYellow = "\033[33m"
|
||||||
|
ColorBlue = "\033[34m"
|
||||||
|
ColorPurple = "\033[35m"
|
||||||
|
ColorCyan = "\033[36m"
|
||||||
|
ColorWhite = "\033[37m"
|
||||||
|
)
|
||||||
19
pkg/utils/db_utils.go
Normal file
19
pkg/utils/db_utils.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsDuplicateKeyError checks if the error is a PostgreSQL duplicate key violation
|
||||||
|
func IsDuplicateKeyError(err error) bool {
|
||||||
|
var pgErr *pgconn.PgError
|
||||||
|
if errors.As(err, &pgErr) {
|
||||||
|
// 23505 is the PostgreSQL error code for unique_violation
|
||||||
|
return pgErr.Code == "23505"
|
||||||
|
}
|
||||||
|
// Fallback for other drivers or if error wrapping is different
|
||||||
|
return strings.Contains(err.Error(), "duplicate key value violates unique constraint")
|
||||||
|
}
|
||||||
112
pkg/utils/email.go
Normal file
112
pkg/utils/email.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gobeyhan/config"
|
||||||
|
"net/smtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SendVerificationEmail(toEmail, token string) error {
|
||||||
|
// Get config
|
||||||
|
host := config.AppConfig.EmailHost
|
||||||
|
port := config.AppConfig.EmailPort
|
||||||
|
from := config.AppConfig.EmailFrom
|
||||||
|
if from == "" {
|
||||||
|
from = "noreply@gauth.local"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct verification link
|
||||||
|
// Assuming frontend handles verification at /verify-email?token=...
|
||||||
|
// Or backend endpoint directly: /api/v1/auth/verify-email?token=...
|
||||||
|
// Let's use APP_URL from config
|
||||||
|
verifyLink := fmt.Sprintf("%s/v1/auth/verify-email?token=%s", config.AppConfig.AppURL, token)
|
||||||
|
|
||||||
|
// Email content
|
||||||
|
subject := "Subject: Verify your email address\n"
|
||||||
|
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
|
||||||
|
body := fmt.Sprintf(`
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>Welcome to GAuth-Central!</h2>
|
||||||
|
<p>Please click the link below to verify your email address:</p>
|
||||||
|
<p><a href="%s">Verify Email</a></p>
|
||||||
|
<p>Or copy and paste this link: %s</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, verifyLink, verifyLink)
|
||||||
|
|
||||||
|
msg := []byte(subject + mime + body)
|
||||||
|
|
||||||
|
// Address
|
||||||
|
addr := fmt.Sprintf("%s:%s", host, port)
|
||||||
|
|
||||||
|
// Auth (if needed)
|
||||||
|
var auth smtp.Auth
|
||||||
|
if config.AppConfig.EmailHostUser != "" && config.AppConfig.EmailHostPassword != "" {
|
||||||
|
auth = smtp.PlainAuth("", config.AppConfig.EmailHostUser, config.AppConfig.EmailHostPassword, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
if err := smtp.SendMail(addr, auth, from, []string{toEmail}, msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendContactEmail(name, email, subject, message, ip string) error {
|
||||||
|
// Get config
|
||||||
|
host := config.AppConfig.EmailHost
|
||||||
|
port := config.AppConfig.EmailPort
|
||||||
|
from := config.AppConfig.EmailFrom
|
||||||
|
if from == "" {
|
||||||
|
from = "noreply@gauth.local"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Typically, contact form emails are sent TO the admin, not the user who filled the form.
|
||||||
|
// However, the original code seemed to imply sending it somewhere.
|
||||||
|
// Let's assume we send it to a configured admin email or the same 'from' address for now.
|
||||||
|
// Or maybe we send a confirmation to the user?
|
||||||
|
// The original python code `send_contact_email` likely sent it to the site admins.
|
||||||
|
// Let's send it to the configured "EmailFrom" address (acting as admin) for this example.
|
||||||
|
toEmail := config.AppConfig.EmailFrom
|
||||||
|
if toEmail == "" {
|
||||||
|
toEmail = "admin@gauth.local"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email content
|
||||||
|
emailSubject := fmt.Sprintf("Subject: New Contact Message: %s\n", subject)
|
||||||
|
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
|
||||||
|
body := fmt.Sprintf(`
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>New Contact Message</h2>
|
||||||
|
<p><strong>Name:</strong> %s</p>
|
||||||
|
<p><strong>Email:</strong> %s</p>
|
||||||
|
<p><strong>IP:</strong> %s</p>
|
||||||
|
<p><strong>Subject:</strong> %s</p>
|
||||||
|
<hr>
|
||||||
|
<p><strong>Message:</strong></p>
|
||||||
|
<p>%s</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, name, email, ip, subject, message)
|
||||||
|
|
||||||
|
msg := []byte(emailSubject + mime + body)
|
||||||
|
|
||||||
|
// Address
|
||||||
|
addr := fmt.Sprintf("%s:%s", host, port)
|
||||||
|
|
||||||
|
// Auth (if needed)
|
||||||
|
var auth smtp.Auth
|
||||||
|
if config.AppConfig.EmailHostUser != "" && config.AppConfig.EmailHostPassword != "" {
|
||||||
|
auth = smtp.PlainAuth("", config.AppConfig.EmailHostUser, config.AppConfig.EmailHostPassword, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
if err := smtp.SendMail(addr, auth, from, []string{toEmail}, msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
137
pkg/utils/image_processor.go
Normal file
137
pkg/utils/image_processor.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"mime/multipart"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gobeyhan/config"
|
||||||
|
|
||||||
|
"github.com/chai2010/webp"
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImageOptions struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Quality float32 // 1-100 (WebP uses float32)
|
||||||
|
Format string // "webp", "jpg", "png"
|
||||||
|
Mode string // "cover", "contain", "resize"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveOptimizedImage processes and saves an image
|
||||||
|
// Returns the relative path to the saved file (e.g., "/uploads/avatars/filename.webp")
|
||||||
|
func SaveOptimizedImage(fileHeader *multipart.FileHeader, uploadDir string, userID string, opts *ImageOptions) (string, error) {
|
||||||
|
// If opts is nil, use defaults from config
|
||||||
|
if opts == nil {
|
||||||
|
opts = &ImageOptions{
|
||||||
|
Width: config.AppConfig.AvatarWidth,
|
||||||
|
Height: config.AppConfig.AvatarHeight,
|
||||||
|
Quality: float32(config.AppConfig.AvatarQuality),
|
||||||
|
Format: config.AppConfig.AvatarFormat,
|
||||||
|
Mode: config.AppConfig.AvatarMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the file
|
||||||
|
srcFile, err := fileHeader.Open()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to open file: %v", err)
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
// Decode image
|
||||||
|
img, _, err := image.Decode(srcFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode image: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize logic
|
||||||
|
if opts.Width > 0 || opts.Height > 0 {
|
||||||
|
switch strings.ToLower(opts.Mode) {
|
||||||
|
case "cover":
|
||||||
|
// Fill requires both dimensions to be effective for cropping.
|
||||||
|
// If one is missing, we fall back to Resize which preserves aspect ratio.
|
||||||
|
if opts.Width > 0 && opts.Height > 0 {
|
||||||
|
img = imaging.Fill(img, opts.Width, opts.Height, imaging.Center, imaging.Lanczos)
|
||||||
|
} else {
|
||||||
|
img = imaging.Resize(img, opts.Width, opts.Height, imaging.Lanczos)
|
||||||
|
}
|
||||||
|
case "contain":
|
||||||
|
// Fit fits the image within the box, preserving aspect ratio.
|
||||||
|
// If one dimension is 0, Fit might behave like Resize(w, h) if implementation allows,
|
||||||
|
// but imaging.Fit usually expects a box.
|
||||||
|
// If one is 0, we assume the user wants to limit the other dimension.
|
||||||
|
if opts.Width > 0 && opts.Height > 0 {
|
||||||
|
img = imaging.Fit(img, opts.Width, opts.Height, imaging.Lanczos)
|
||||||
|
} else {
|
||||||
|
img = imaging.Resize(img, opts.Width, opts.Height, imaging.Lanczos)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// "resize" or empty: Resize preserves aspect ratio if one arg is 0.
|
||||||
|
// If both are provided, it stretches unless we use Fill/Fit.
|
||||||
|
// imaging.Resize stretches if both are non-zero.
|
||||||
|
img = imaging.Resize(img, opts.Width, opts.Height, imaging.Lanczos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine output format and filename
|
||||||
|
ext := "." + strings.ToLower(opts.Format)
|
||||||
|
if opts.Format == "" {
|
||||||
|
ext = ".webp" // Default to WebP
|
||||||
|
opts.Format = "webp"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate filename
|
||||||
|
filename := fmt.Sprintf("%s_%d%s", userID, time.Now().UnixNano(), ext)
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(uploadDir, filename)
|
||||||
|
outFile, err := os.Create(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create output file: %v", err)
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
// Encode
|
||||||
|
switch strings.ToLower(opts.Format) {
|
||||||
|
case "jpg", "jpeg":
|
||||||
|
quality := int(opts.Quality)
|
||||||
|
if quality < 1 || quality > 100 {
|
||||||
|
quality = 90
|
||||||
|
}
|
||||||
|
err = jpeg.Encode(outFile, img, &jpeg.Options{Quality: quality})
|
||||||
|
case "png":
|
||||||
|
err = png.Encode(outFile, img) // PNG is lossless
|
||||||
|
case "webp", "":
|
||||||
|
err = webp.Encode(outFile, img, &webp.Options{Lossless: false, Quality: opts.Quality})
|
||||||
|
default:
|
||||||
|
// Fallback to WebP
|
||||||
|
err = webp.Encode(outFile, img, &webp.Options{Lossless: false, Quality: opts.Quality})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to encode image: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return relative path
|
||||||
|
relPath := filepath.Join(uploadDir, filename)
|
||||||
|
if strings.HasPrefix(relPath, ".") {
|
||||||
|
relPath = relPath[1:]
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(relPath, "/") {
|
||||||
|
relPath = "/" + relPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return relPath, nil
|
||||||
|
}
|
||||||
15
pkg/utils/password.go
Normal file
15
pkg/utils/password.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPasswordHash(password, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
15
pkg/utils/token.go
Normal file
15
pkg/utils/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
|
||||||
|
}
|
||||||
1505
seed_force.log
Normal file
1505
seed_force.log
Normal file
File diff suppressed because it is too large
Load Diff
1499
seed_run.log
Normal file
1499
seed_run.log
Normal file
File diff suppressed because it is too large
Load Diff
1235
server.log
Normal file
1235
server.log
Normal file
File diff suppressed because it is too large
Load Diff
1
update_log.txt
Normal file
1
update_log.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# This is a dummy file to update user.
|
||||||
BIN
uploads/blog/post_1770861638665070000.webp
Normal file
BIN
uploads/blog/post_1770861638665070000.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
BIN
uploads/posts/blog_1770860097523301000.webp
Normal file
BIN
uploads/posts/blog_1770860097523301000.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
403
views/admin/blog/blog.templ
Normal file
403
views/admin/blog/blog.templ
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
package blog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ List(posts []models.Post) {
|
||||||
|
@Layout("Blog Posts") {
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">Blog Posts</h1>
|
||||||
|
<a href="/admin/blog/new" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Add Post</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Categories</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
for _, post := range posts {
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ fmt.Sprintf("%d", post.ID) }</td>
|
||||||
|
<td class="px-6 py-4 text-sm font-medium text-gray-900">
|
||||||
|
<div class="max-w-xs truncate">{ post.Title }</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
if len(post.Categories) > 0 {
|
||||||
|
for i, cat := range post.Categories {
|
||||||
|
if i > 0 {
|
||||||
|
<span>, </span>
|
||||||
|
}
|
||||||
|
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">{ cat.Title }</span>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<span class="text-gray-400">-</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
if post.IsActive {
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span>
|
||||||
|
} else {
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Inactive</span>
|
||||||
|
}
|
||||||
|
if post.IsFront {
|
||||||
|
<span class="ml-1 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-purple-100 text-purple-800">Front</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{ post.CreatedAt.Format("2006-01-02") }
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/admin/blog/%d/edit", post.ID)) } class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</a>
|
||||||
|
<form id={ fmt.Sprintf("delete-blog-%d", post.ID) } action={ templ.SafeURL(fmt.Sprintf("/admin/blog/%d/delete", post.ID)) } method="POST" class="inline">
|
||||||
|
<button type="button" class="text-red-600 hover:text-red-900" onclick={ templ.ComponentScript{ Call: fmt.Sprintf("confirmDelete('Delete Post', 'Are you sure you want to delete this post?', 'delete-blog-%d')", post.ID) } }>Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Create(categories []models.Category, tags []models.Tag, errors map[string]string) {
|
||||||
|
@Layout("Create Blog Post") {
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Create New Post</h1>
|
||||||
|
<form id="blog-form" action="/admin/blog" method="POST" enctype="multipart/form-data" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div>
|
||||||
|
<label for="title" class="block text-sm font-medium text-gray-700">Title *</label>
|
||||||
|
<input type="text" name="title" id="title" required
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
if errors["title"] != "" {
|
||||||
|
<p class="mt-2 text-sm text-red-600">{ errors["title"] }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="slug" class="block text-sm font-medium text-gray-700">Slug</label>
|
||||||
|
<input type="text" name="slug" id="slug"
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
<p class="mt-1 text-xs text-gray-400 italic">Leave blank to generate automatically from title.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="content" class="block text-sm font-medium text-gray-700">Content *</label>
|
||||||
|
<div id="editor" class="mt-1 bg-white h-64 border rounded-md"></div>
|
||||||
|
<input type="hidden" name="content" id="content-input" required />
|
||||||
|
if errors["content"] != "" {
|
||||||
|
<p class="mt-2 text-sm text-red-600">{ errors["content"] }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="keywords" class="block text-sm font-medium text-gray-700">Keywords</label>
|
||||||
|
<input type="text" name="keywords" id="keywords"
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="image_file" class="block text-sm font-medium text-gray-700">Upload Image</label>
|
||||||
|
<input type="file" name="image_file" id="image_file" accept="image/*"
|
||||||
|
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="image" class="block text-sm font-medium text-gray-700">Manual Image Path (optional)</label>
|
||||||
|
<input type="text" name="image" id="image"
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Categories</label>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
if len(categories) > 0 {
|
||||||
|
for _, cat := range categories {
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="category_ids" value={ fmt.Sprintf("%d", cat.ID) }
|
||||||
|
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||||
|
<span class="ml-2 text-sm text-gray-700">{ cat.Title }</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<p class="text-xs text-gray-500 italic col-span-2">No active categories found. <a href="/admin/blog/categories/new" class="text-indigo-600 underline">Add one first.</a></p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Tags</label>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
if len(tags) > 0 {
|
||||||
|
for _, tag := range tags {
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="tag_ids" value={ fmt.Sprintf("%d", tag.ID) }
|
||||||
|
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||||
|
<span class="ml-2 text-sm text-gray-700">{ tag.Tag }</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<p class="text-xs text-gray-500 italic col-span-3">No active tags found. <a href="/admin/blog/tags/new" class="text-indigo-600 underline">Add one first.</a></p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="is_active" checked
|
||||||
|
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Active</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="is_front" checked
|
||||||
|
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Show on Front Page</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/blog" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
|
||||||
|
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Create Post</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
autoSlug('title', 'slug');
|
||||||
|
|
||||||
|
function initQuill() {
|
||||||
|
if (typeof Quill === 'undefined') {
|
||||||
|
setTimeout(initQuill, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorEl = document.querySelector('#editor');
|
||||||
|
if (!editorEl || editorEl.classList.contains('ql-container')) return;
|
||||||
|
|
||||||
|
const quill = new Quill('#editor', {
|
||||||
|
theme: 'snow',
|
||||||
|
modules: {
|
||||||
|
toolbar: [
|
||||||
|
[{ 'header': [1, 2, 3, false] }],
|
||||||
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
|
['link', 'image', 'video'],
|
||||||
|
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||||
|
['clean']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = editorEl.closest('form');
|
||||||
|
if (form) {
|
||||||
|
const syncContent = () => {
|
||||||
|
const contentInput = document.querySelector('#content-input');
|
||||||
|
if (contentInput) contentInput.value = quill.root.innerHTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
form.addEventListener('submit', syncContent);
|
||||||
|
form.addEventListener('htmx:configRequest', (event) => {
|
||||||
|
syncContent();
|
||||||
|
if (event.detail.parameters) {
|
||||||
|
event.detail.parameters['content'] = quill.root.innerHTML;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initQuill();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Edit(post *models.Post, categories []models.Category, tags []models.Tag, errors map[string]string) {
|
||||||
|
@Layout("Edit Blog Post") {
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Post: { post.Title }</h1>
|
||||||
|
<form id="blog-form-edit" action={ templ.SafeURL(fmt.Sprintf("/admin/blog/%d", post.ID)) } method="POST" enctype="multipart/form-data" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div>
|
||||||
|
<label for="title" class="block text-sm font-medium text-gray-700">Title *</label>
|
||||||
|
<input type="text" name="title" id="title" value={ post.Title } required
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
if errors["title"] != "" {
|
||||||
|
<p class="mt-2 text-sm text-red-600">{ errors["title"] }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="slug" class="block text-sm font-medium text-gray-700">Slug</label>
|
||||||
|
<input type="text" name="slug" id="slug" value={ post.Slug }
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
<p class="mt-1 text-xs text-gray-400 italic">Leave blank to keep existing or regenerate from title.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="content" class="block text-sm font-medium text-gray-700">Content *</label>
|
||||||
|
<div id="editor" class="mt-1 bg-white h-64 border rounded-md">@templ.Raw(post.Content)</div>
|
||||||
|
<input type="hidden" name="content" id="content-input" value={ post.Content } required />
|
||||||
|
if errors["content"] != "" {
|
||||||
|
<p class="mt-2 text-sm text-red-600">{ errors["content"] }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="keywords" class="block text-sm font-medium text-gray-700">Keywords</label>
|
||||||
|
<input type="text" name="keywords" id="keywords" value={ post.Keywords }
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="image_file" class="block text-sm font-medium text-gray-700">Update Image</label>
|
||||||
|
<input type="file" name="image_file" id="image_file" accept="image/*"
|
||||||
|
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100" />
|
||||||
|
if post.Image != "" {
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-xs text-gray-500 mb-1">Current Preview:</p>
|
||||||
|
<img src={ post.Image } class="h-20 w-auto rounded border shadow-sm" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="image" class="block text-sm font-medium text-gray-700">Current Image Path</label>
|
||||||
|
<input type="text" name="image" id="image" value={ post.Image }
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Categories</label>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
for _, cat := range categories {
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="category_ids" value={ fmt.Sprintf("%d", cat.ID) }
|
||||||
|
if isCategorySelected(post.Categories, cat.ID) {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||||
|
<span class="ml-2 text-sm text-gray-700">{ cat.Title }</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Tags</label>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
for _, tag := range tags {
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="tag_ids" value={ fmt.Sprintf("%d", tag.ID) }
|
||||||
|
if isTagSelected(post.Tags, tag.ID) {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||||
|
<span class="ml-2 text-sm text-gray-700">{ tag.Tag }</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="is_active"
|
||||||
|
if post.IsActive {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Active</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="is_front"
|
||||||
|
if post.IsFront {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Show on Front Page</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/blog" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
|
||||||
|
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
autoSlug('title', 'slug');
|
||||||
|
|
||||||
|
function initQuillEdit() {
|
||||||
|
if (typeof Quill === 'undefined') {
|
||||||
|
setTimeout(initQuillEdit, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorEl = document.querySelector('#editor');
|
||||||
|
if (!editorEl || editorEl.classList.contains('ql-container')) return;
|
||||||
|
|
||||||
|
const quill = new Quill('#editor', {
|
||||||
|
theme: 'snow',
|
||||||
|
modules: {
|
||||||
|
toolbar: [
|
||||||
|
[{ 'header': [1, 2, 3, false] }],
|
||||||
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
|
['link', 'image', 'video'],
|
||||||
|
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||||
|
['clean']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = editorEl.closest('form');
|
||||||
|
if (form) {
|
||||||
|
const syncContent = () => {
|
||||||
|
const contentInput = document.querySelector('#content-input');
|
||||||
|
if (contentInput) contentInput.value = quill.root.innerHTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
form.addEventListener('submit', syncContent);
|
||||||
|
form.addEventListener('htmx:configRequest', (event) => {
|
||||||
|
syncContent();
|
||||||
|
if (event.detail.parameters) {
|
||||||
|
event.detail.parameters['content'] = quill.root.innerHTML;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initQuillEdit();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCategorySelected(categories []*models.Category, id uint64) bool {
|
||||||
|
for _, cat := range categories {
|
||||||
|
if cat.ID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTagSelected(tags []*models.Tag, id uint64) bool {
|
||||||
|
for _, tag := range tags {
|
||||||
|
if tag.ID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
749
views/admin/blog/blog_templ.go
Normal file
749
views/admin/blog/blog_templ.go
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package blog
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func List(posts []models.Post) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"px-4 py-6 sm:px-0\"><div class=\"flex justify-between items-center mb-6\"><h1 class=\"text-2xl font-semibold text-gray-900\">Blog Posts</h1><a href=\"/admin/blog/new\" class=\"bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700\">Add Post</a></div><div class=\"bg-white shadow overflow-hidden sm:rounded-lg\"><table class=\"min-w-full divide-y divide-gray-200\"><thead class=\"bg-gray-50\"><tr><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">ID</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Title</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Categories</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Status</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Created</th><th class=\"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\">Actions</th></tr></thead> <tbody class=\"bg-white divide-y divide-gray-200\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, post := range posts {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<tr><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", post.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 31, Col: 122}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</td><td class=\"px-6 py-4 text-sm font-medium text-gray-900\"><div class=\"max-w-xs truncate\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 33, Col: 79}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(post.Categories) > 0 {
|
||||||
|
for i, cat := range post.Categories {
|
||||||
|
if i > 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span>, </span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " <span class=\"text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(cat.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 41, Col: 121}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"text-gray-400\">-</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if post.IsActive {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800\">Active</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800\">Inactive</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if post.IsFront {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"ml-1 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-purple-100 text-purple-800\">Front</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(post.CreatedAt.Format("2006-01-02"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 58, Col: 73}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</td><td class=\"px-6 py-4 whitespace-nowrap text-right text-sm font-medium\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/blog/%d/edit", post.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 61, Col: 104}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" class=\"text-indigo-600 hover:text-indigo-900 mr-4\">Edit</a><form id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-blog-%d", post.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 62, Col: 85}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/blog/%d/delete", post.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 62, Col: 157}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" method=\"POST\" class=\"inline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('Delete Post', 'Are you sure you want to delete this post?', 'delete-blog-%d')", post.ID)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<button type=\"button\" class=\"text-red-600 hover:text-red-900\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('Delete Post', 'Are you sure you want to delete this post?', 'delete-blog-%d')", post.ID)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">Delete</button></form></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</tbody></table></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Blog Posts").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(categories []models.Category, tags []models.Tag, errors map[string]string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var11 == nil {
|
||||||
|
templ_7745c5c3_Var11 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var12 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"px-4 py-6 sm:px-0\"><div class=\"max-w-4xl mx-auto\"><h1 class=\"text-2xl font-semibold text-gray-900 mb-6\">Create New Post</h1><form id=\"blog-form\" action=\"/admin/blog\" method=\"POST\" enctype=\"multipart/form-data\" class=\"space-y-6 bg-white p-6 rounded-lg shadow\"><div><label for=\"title\" class=\"block text-sm font-medium text-gray-700\">Title *</label> <input type=\"text\" name=\"title\" id=\"title\" required class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if errors["title"] != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p class=\"mt-2 text-sm text-red-600\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(errors["title"])
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 86, Col: 82}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><div><label for=\"slug\" class=\"block text-sm font-medium text-gray-700\">Slug</label> <input type=\"text\" name=\"slug\" id=\"slug\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"><p class=\"mt-1 text-xs text-gray-400 italic\">Leave blank to generate automatically from title.</p></div><div><label for=\"content\" class=\"block text-sm font-medium text-gray-700\">Content *</label><div id=\"editor\" class=\"mt-1 bg-white h-64 border rounded-md\"></div><input type=\"hidden\" name=\"content\" id=\"content-input\" required> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if errors["content"] != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<p class=\"mt-2 text-sm text-red-600\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(errors["content"])
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 102, Col: 84}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div><div><label for=\"keywords\" class=\"block text-sm font-medium text-gray-700\">Keywords</label> <input type=\"text\" name=\"keywords\" id=\"keywords\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"></div><div class=\"grid grid-cols-2 gap-4\"><div><label for=\"image_file\" class=\"block text-sm font-medium text-gray-700\">Upload Image</label> <input type=\"file\" name=\"image_file\" id=\"image_file\" accept=\"image/*\" class=\"mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100\"></div><div><label for=\"image\" class=\"block text-sm font-medium text-gray-700\">Manual Image Path (optional)</label> <input type=\"text\" name=\"image\" id=\"image\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"></div></div><div><label class=\"block text-sm font-medium text-gray-700 mb-2\">Categories</label><div class=\"grid grid-cols-2 gap-2\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(categories) > 0 {
|
||||||
|
for _, cat := range categories {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<label class=\"flex items-center\"><input type=\"checkbox\" name=\"category_ids\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", cat.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 131, Col: 116}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"> <span class=\"ml-2 text-sm text-gray-700\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(cat.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 133, Col: 92}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</span></label>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<p class=\"text-xs text-gray-500 italic col-span-2\">No active categories found. <a href=\"/admin/blog/categories/new\" class=\"text-indigo-600 underline\">Add one first.</a></p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div></div><div><label class=\"block text-sm font-medium text-gray-700 mb-2\">Tags</label><div class=\"grid grid-cols-3 gap-2\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(tags) > 0 {
|
||||||
|
for _, tag := range tags {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<label class=\"flex items-center\"><input type=\"checkbox\" name=\"tag_ids\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", tag.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 148, Col: 111}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" class=\"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"> <span class=\"ml-2 text-sm text-gray-700\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 string
|
||||||
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(tag.Tag)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 150, Col: 90}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></label>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<p class=\"text-xs text-gray-500 italic col-span-3\">No active tags found. <a href=\"/admin/blog/tags/new\" class=\"text-indigo-600 underline\">Add one first.</a></p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div></div><div class=\"flex items-center space-x-4\"><label class=\"flex items-center\"><input type=\"checkbox\" name=\"is_active\" checked class=\"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"> <span class=\"ml-2 text-sm text-gray-700\">Active</span></label> <label class=\"flex items-center\"><input type=\"checkbox\" name=\"is_front\" checked class=\"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"> <span class=\"ml-2 text-sm text-gray-700\">Show on Front Page</span></label></div><div class=\"flex justify-end gap-3\"><a href=\"/admin/blog\" class=\"bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50\">Cancel</a> <button type=\"submit\" class=\"bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700\">Create Post</button></div></form><script>\n autoSlug('title', 'slug');\n \n function initQuill() {\n if (typeof Quill === 'undefined') {\n setTimeout(initQuill, 100);\n return;\n }\n \n const editorEl = document.querySelector('#editor');\n if (!editorEl || editorEl.classList.contains('ql-container')) return;\n\n const quill = new Quill('#editor', {\n theme: 'snow',\n modules: {\n toolbar: [\n [{ 'header': [1, 2, 3, false] }],\n ['bold', 'italic', 'underline', 'strike'],\n ['link', 'image', 'video'],\n [{ 'list': 'ordered'}, { 'list': 'bullet' }],\n ['clean']\n ]\n }\n });\n\n const form = editorEl.closest('form');\n if (form) {\n const syncContent = () => {\n const contentInput = document.querySelector('#content-input');\n if (contentInput) contentInput.value = quill.root.innerHTML;\n };\n\n form.addEventListener('submit', syncContent);\n form.addEventListener('htmx:configRequest', (event) => {\n syncContent();\n if (event.detail.parameters) {\n event.detail.parameters['content'] = quill.root.innerHTML;\n }\n });\n }\n }\n initQuill();\n </script></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Create Blog Post").Render(templ.WithChildren(ctx, templ_7745c5c3_Var12), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Edit(post *models.Post, categories []models.Category, tags []models.Tag, errors map[string]string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var19 == nil {
|
||||||
|
templ_7745c5c3_Var19 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var20 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div class=\"px-4 py-6 sm:px-0\"><div class=\"max-w-4xl mx-auto\"><h1 class=\"text-2xl font-semibold text-gray-900 mb-6\">Edit Post: ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var21 string
|
||||||
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 230, Col: 93}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</h1><form id=\"blog-form-edit\" action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/blog/%d", post.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 231, Col: 104}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" method=\"POST\" enctype=\"multipart/form-data\" class=\"space-y-6 bg-white p-6 rounded-lg shadow\"><div><label for=\"title\" class=\"block text-sm font-medium text-gray-700\">Title *</label> <input type=\"text\" name=\"title\" id=\"title\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var23 string
|
||||||
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(post.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 234, Col: 85}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" required class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if errors["title"] != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<p class=\"mt-2 text-sm text-red-600\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var24 string
|
||||||
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(errors["title"])
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 237, Col: 82}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</div><div><label for=\"slug\" class=\"block text-sm font-medium text-gray-700\">Slug</label> <input type=\"text\" name=\"slug\" id=\"slug\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var25 string
|
||||||
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(post.Slug)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 243, Col: 82}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"><p class=\"mt-1 text-xs text-gray-400 italic\">Leave blank to keep existing or regenerate from title.</p></div><div><label for=\"content\" class=\"block text-sm font-medium text-gray-700\">Content *</label><div id=\"editor\" class=\"mt-1 bg-white h-64 border rounded-md\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.Raw(post.Content).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</div><input type=\"hidden\" name=\"content\" id=\"content-input\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var26 string
|
||||||
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(post.Content)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 251, Col: 99}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" required> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if errors["content"] != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<p class=\"mt-2 text-sm text-red-600\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var27 string
|
||||||
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(errors["content"])
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 253, Col: 84}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div><div><label for=\"keywords\" class=\"block text-sm font-medium text-gray-700\">Keywords</label> <input type=\"text\" name=\"keywords\" id=\"keywords\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var28 string
|
||||||
|
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(post.Keywords)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 259, Col: 94}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"></div><div class=\"grid grid-cols-2 gap-4\"><div><label for=\"image_file\" class=\"block text-sm font-medium text-gray-700\">Update Image</label> <input type=\"file\" name=\"image_file\" id=\"image_file\" accept=\"image/*\" class=\"mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100\"> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if post.Image != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<div class=\"mt-2\"><p class=\"text-xs text-gray-500 mb-1\">Current Preview:</p><img src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var29 string
|
||||||
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(post.Image)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 271, Col: 57}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" class=\"h-20 w-auto rounded border shadow-sm\"></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</div><div><label for=\"image\" class=\"block text-sm font-medium text-gray-700\">Current Image Path</label> <input type=\"text\" name=\"image\" id=\"image\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var30 string
|
||||||
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(post.Image)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 277, Col: 89}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"></div></div><div><label class=\"block text-sm font-medium text-gray-700 mb-2\">Categories</label><div class=\"grid grid-cols-2 gap-2\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, cat := range categories {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<label class=\"flex items-center\"><input type=\"checkbox\" name=\"category_ids\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var31 string
|
||||||
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", cat.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 287, Col: 112}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if isCategorySelected(post.Categories, cat.ID) {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, " checked")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " class=\"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"> <span class=\"ml-2 text-sm text-gray-700\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var32 string
|
||||||
|
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(cat.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 292, Col: 88}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</span></label>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</div></div><div><label class=\"block text-sm font-medium text-gray-700 mb-2\">Tags</label><div class=\"grid grid-cols-3 gap-2\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, tag := range tags {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<label class=\"flex items-center\"><input type=\"checkbox\" name=\"tag_ids\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var33 string
|
||||||
|
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", tag.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 303, Col: 107}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if isTagSelected(post.Tags, tag.ID) {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " checked")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " class=\"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"> <span class=\"ml-2 text-sm text-gray-700\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var34 string
|
||||||
|
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(tag.Tag)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/blog.templ`, Line: 308, Col: 86}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</span></label>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</div></div><div class=\"flex items-center space-x-4\"><label class=\"flex items-center\"><input type=\"checkbox\" name=\"is_active\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if post.IsActive {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " checked")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, " class=\"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"> <span class=\"ml-2 text-sm text-gray-700\">Active</span></label> <label class=\"flex items-center\"><input type=\"checkbox\" name=\"is_front\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if post.IsFront {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " checked")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, " class=\"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"> <span class=\"ml-2 text-sm text-gray-700\">Show on Front Page</span></label></div><div class=\"flex justify-end gap-3\"><a href=\"/admin/blog\" class=\"bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50\">Cancel</a> <button type=\"submit\" class=\"bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700\">Save Changes</button></div></form><script>\n autoSlug('title', 'slug');\n \n function initQuillEdit() {\n if (typeof Quill === 'undefined') {\n setTimeout(initQuillEdit, 100);\n return;\n }\n \n const editorEl = document.querySelector('#editor');\n if (!editorEl || editorEl.classList.contains('ql-container')) return;\n\n const quill = new Quill('#editor', {\n theme: 'snow',\n modules: {\n toolbar: [\n [{ 'header': [1, 2, 3, false] }],\n ['bold', 'italic', 'underline', 'strike'],\n ['link', 'image', 'video'],\n [{ 'list': 'ordered'}, { 'list': 'bullet' }],\n ['clean']\n ]\n }\n });\n\n const form = editorEl.closest('form');\n if (form) {\n const syncContent = () => {\n const contentInput = document.querySelector('#content-input');\n if (contentInput) contentInput.value = quill.root.innerHTML;\n };\n\n form.addEventListener('submit', syncContent);\n form.addEventListener('htmx:configRequest', (event) => {\n syncContent();\n if (event.detail.parameters) {\n event.detail.parameters['content'] = quill.root.innerHTML;\n }\n });\n }\n }\n initQuillEdit();\n </script></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Edit Blog Post").Render(templ.WithChildren(ctx, templ_7745c5c3_Var20), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCategorySelected(categories []*models.Category, id uint64) bool {
|
||||||
|
for _, cat := range categories {
|
||||||
|
if cat.ID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTagSelected(tags []*models.Tag, id uint64) bool {
|
||||||
|
for _, tag := range tags {
|
||||||
|
if tag.ID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
150
views/admin/blog/category.templ
Normal file
150
views/admin/blog/category.templ
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package blog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ CategoryList(items []models.Category) {
|
||||||
|
@Layout("Blog Categories") {
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">Categories</h1>
|
||||||
|
<a href="/admin/blog/categories/new" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Add Category</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Slug</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Order</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Active</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
for _, cat := range items {
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ fmt.Sprintf("%d", cat.ID) }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{ cat.Title }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ cat.Slug }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ fmt.Sprintf("%d", cat.Order) }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
if cat.IsActive {
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span>
|
||||||
|
} else {
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Inactive</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/admin/blog/categories/%d/edit", cat.ID)) } class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</a>
|
||||||
|
<form id={ fmt.Sprintf("delete-cat-%d", cat.ID) } action={ templ.SafeURL(fmt.Sprintf("/admin/blog/categories/%d/delete", cat.ID)) } method="POST" class="inline">
|
||||||
|
<button type="button" class="text-red-600 hover:text-red-900" onclick={ templ.ComponentScript{ Call: fmt.Sprintf("confirmDelete('Delete Category', 'Delete this category and all associations?', 'delete-cat-%d')", cat.ID) } }>Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ CategoryForm(cat *models.Category, categories []models.Category, errors map[string]string) {
|
||||||
|
@Layout(ifElse(cat.ID == 0, "Create Category", "Edit Category")) {
|
||||||
|
<div class="max-w-2xl mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">
|
||||||
|
if cat.ID == 0 {
|
||||||
|
Create New Category
|
||||||
|
} else {
|
||||||
|
Edit Category: { cat.Title }
|
||||||
|
}
|
||||||
|
</h1>
|
||||||
|
<form action={ templ.SafeURL(ifElse(cat.ID == 0, "/admin/blog/categories", fmt.Sprintf("/admin/blog/categories/%d", cat.ID))) } method="POST" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div>
|
||||||
|
<label for="title" class="block text-sm font-medium text-gray-700">Title*</label>
|
||||||
|
<input type="text" name="title" id="title" value={ cat.Title } required
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
if errors["title"] != "" {
|
||||||
|
<p class="mt-2 text-sm text-red-600">{ errors["title"] }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="slug" class="block text-sm font-medium text-gray-700">Slug (leave empty to auto-generate)</label>
|
||||||
|
<input type="text" name="slug" id="slug" value={ cat.Slug }
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="parent_id" class="block text-sm font-medium text-gray-700">Parent Category</label>
|
||||||
|
<select name="parent_id" id="parent_id" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border">
|
||||||
|
<option value="">None</option>
|
||||||
|
for _, c := range categories {
|
||||||
|
if c.ID != cat.ID {
|
||||||
|
<option value={ fmt.Sprintf("%d", c.ID) }
|
||||||
|
if cat.ParentID != nil && *cat.ParentID == c.ID {
|
||||||
|
selected
|
||||||
|
}
|
||||||
|
>{ c.Title }</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="order" class="block text-sm font-medium text-gray-700">Display Order</label>
|
||||||
|
<input type="number" name="order" id="order" value={ fmt.Sprintf("%d", cat.Order) }
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end pb-2">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="is_active"
|
||||||
|
if cat.IsActive || cat.ID == 0 {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Active</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="description" class="block text-sm font-medium text-gray-700">Meta Description</label>
|
||||||
|
<textarea name="description" id="description" rows="3"
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border">{ cat.Desc }</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="keywords" class="block text-sm font-medium text-gray-700">Meta Keywords</label>
|
||||||
|
<input type="text" name="keywords" id="keywords" value={ cat.Keywords }
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/blog/categories" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
|
||||||
|
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">
|
||||||
|
if cat.ID == 0 {
|
||||||
|
Create Category
|
||||||
|
} else {
|
||||||
|
Save Changes
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
autoSlug('title', 'slug');
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ifElse(cond bool, a, b string) string {
|
||||||
|
if cond {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
448
views/admin/blog/category_templ.go
Normal file
448
views/admin/blog/category_templ.go
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package blog
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CategoryList(items []models.Category) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex justify-between items-center mb-6\"><h1 class=\"text-2xl font-semibold text-gray-900\">Categories</h1><a href=\"/admin/blog/categories/new\" class=\"bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700\">Add Category</a></div><div class=\"bg-white shadow overflow-hidden sm:rounded-lg\"><table class=\"min-w-full divide-y divide-gray-200\"><thead class=\"bg-gray-50\"><tr><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">ID</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Title</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Slug</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Order</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Active</th><th class=\"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\">Actions</th></tr></thead> <tbody class=\"bg-white divide-y divide-gray-200\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, cat := range items {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<tr><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", cat.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 30, Col: 117}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(cat.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 31, Col: 113}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(cat.Slug)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 32, Col: 100}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", cat.Order))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 33, Col: 120}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if cat.IsActive {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800\">Active</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800\">Inactive</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td class=\"px-6 py-4 whitespace-nowrap text-right text-sm font-medium\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/blog/categories/%d/edit", cat.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 42, Col: 110}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"text-indigo-600 hover:text-indigo-900 mr-4\">Edit</a><form id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-cat-%d", cat.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 43, Col: 79}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/blog/categories/%d/delete", cat.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 43, Col: 161}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" method=\"POST\" class=\"inline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('Delete Category', 'Delete this category and all associations?', 'delete-cat-%d')", cat.ID)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<button type=\"button\" class=\"text-red-600 hover:text-red-900\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('Delete Category', 'Delete this category and all associations?', 'delete-cat-%d')", cat.ID)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">Delete</button></form></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</tbody></table></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Blog Categories").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CategoryForm(cat *models.Category, categories []models.Category, errors map[string]string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var11 == nil {
|
||||||
|
templ_7745c5c3_Var11 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var12 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"max-w-2xl mx-auto py-6\"><h1 class=\"text-2xl font-semibold text-gray-900 mb-6\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if cat.ID == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Create New Category")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Edit Category: ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(cat.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 62, Col: 46}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</h1><form action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(ifElse(cat.ID == 0, "/admin/blog/categories", fmt.Sprintf("/admin/blog/categories/%d", cat.ID))))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 65, Col: 137}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" method=\"POST\" class=\"space-y-6 bg-white p-6 rounded-lg shadow\"><div><label for=\"title\" class=\"block text-sm font-medium text-gray-700\">Title*</label> <input type=\"text\" name=\"title\" id=\"title\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(cat.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 68, Col: 80}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" required class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if errors["title"] != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p class=\"mt-2 text-sm text-red-600\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(errors["title"])
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 71, Col: 78}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><div><label for=\"slug\" class=\"block text-sm font-medium text-gray-700\">Slug (leave empty to auto-generate)</label> <input type=\"text\" name=\"slug\" id=\"slug\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(cat.Slug)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 77, Col: 77}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"></div><div><label for=\"parent_id\" class=\"block text-sm font-medium text-gray-700\">Parent Category</label> <select name=\"parent_id\" id=\"parent_id\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"><option value=\"\">None</option> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, c := range categories {
|
||||||
|
if c.ID != cat.ID {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<option value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 string
|
||||||
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", c.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 87, Col: 71}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if cat.ParentID != nil && *cat.ParentID == c.ID {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var19 string
|
||||||
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(c.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 91, Col: 42}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</select></div><div class=\"grid grid-cols-2 gap-4\"><div><label for=\"order\" class=\"block text-sm font-medium text-gray-700\">Display Order</label> <input type=\"number\" name=\"order\" id=\"order\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var20 string
|
||||||
|
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", cat.Order))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 100, Col: 105}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"></div><div class=\"flex items-end pb-2\"><label class=\"flex items-center\"><input type=\"checkbox\" name=\"is_active\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if cat.IsActive || cat.ID == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " checked")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " class=\"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"> <span class=\"ml-2 text-sm text-gray-700\">Active</span></label></div></div><div><label for=\"description\" class=\"block text-sm font-medium text-gray-700\">Meta Description</label> <textarea name=\"description\" id=\"description\" rows=\"3\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var21 string
|
||||||
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(cat.Desc)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 118, Col: 165}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</textarea></div><div><label for=\"keywords\" class=\"block text-sm font-medium text-gray-700\">Meta Keywords</label> <input type=\"text\" name=\"keywords\" id=\"keywords\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 string
|
||||||
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(cat.Keywords)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/category.templ`, Line: 123, Col: 89}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"></div><div class=\"flex justify-end gap-3\"><a href=\"/admin/blog/categories\" class=\"bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50\">Cancel</a> <button type=\"submit\" class=\"bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if cat.ID == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "Create Category")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "Save Changes")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</button></div></form><script>\n autoSlug('title', 'slug');\n </script></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout(ifElse(cat.ID == 0, "Create Category", "Edit Category")).Render(templ.WithChildren(ctx, templ_7745c5c3_Var12), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ifElse(cond bool, a, b string) string {
|
||||||
|
if cond {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
96
views/admin/blog/comment.templ
Normal file
96
views/admin/blog/comment.templ
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package blog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ CommentList(items []models.Comment) {
|
||||||
|
@Layout("Blog Comments") {
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">Comments</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Post</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Author</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Comment</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
for _, comment := range items {
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ fmt.Sprintf("%d", comment.ID) }</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-900">
|
||||||
|
<div class="max-w-xs truncate">{ comment.Product.Title }</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
User ID: { fmt.Sprintf("%d", comment.UserID) }
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-500">
|
||||||
|
<div class="max-w-md line-clamp-2">{ comment.Body }</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
if comment.IsActive {
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Approved</span>
|
||||||
|
} else {
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">Pending</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/admin/blog/comments/%d/edit", comment.ID)) } class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</a>
|
||||||
|
<form id={ fmt.Sprintf("delete-comment-%d", comment.ID) } action={ templ.SafeURL(fmt.Sprintf("/admin/blog/comments/%d/delete", comment.ID)) } method="POST" class="inline">
|
||||||
|
<button type="button" class="text-red-600 hover:text-red-900" onclick={ templ.ComponentScript{ Call: fmt.Sprintf("confirmDelete('Delete Comment', 'Are you sure you want to delete this comment?', 'delete-comment-%d')", comment.ID) } }>Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ CommentForm(comment *models.Comment, errors map[string]string) {
|
||||||
|
@Layout("Edit Comment") {
|
||||||
|
<div class="max-w-2xl mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Comment</h1>
|
||||||
|
<form action={ templ.SafeURL(fmt.Sprintf("/admin/blog/comments/%d", comment.ID)) } method="POST" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">User ID</label>
|
||||||
|
<p class="mt-1 p-2 bg-gray-50 border rounded text-sm text-gray-900">{ fmt.Sprintf("%d", comment.UserID) }</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="body" class="block text-sm font-medium text-gray-700">Comment Body</label>
|
||||||
|
<textarea name="body" id="body" rows="5" required
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border">{ comment.Body }</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" name="is_active"
|
||||||
|
if comment.IsActive {
|
||||||
|
checked
|
||||||
|
}
|
||||||
|
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
|
||||||
|
<span class="ml-2 text-sm text-gray-700">Approved / Active</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/blog/comments" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
|
||||||
|
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
292
views/admin/blog/comment_templ.go
Normal file
292
views/admin/blog/comment_templ.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package blog
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CommentList(items []models.Comment) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex justify-between items-center mb-6\"><h1 class=\"text-2xl font-semibold text-gray-900\">Comments</h1></div><div class=\"bg-white shadow overflow-hidden sm:rounded-lg\"><table class=\"min-w-full divide-y divide-gray-200\"><thead class=\"bg-gray-50\"><tr><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">ID</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Post</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Author</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Comment</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Status</th><th class=\"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\">Actions</th></tr></thead> <tbody class=\"bg-white divide-y divide-gray-200\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, comment := range items {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<tr><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", comment.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/comment.templ`, Line: 29, Col: 121}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</td><td class=\"px-6 py-4 text-sm text-gray-900\"><div class=\"max-w-xs truncate\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(comment.Product.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/comment.templ`, Line: 31, Col: 86}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">User ID: ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", comment.UserID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/comment.templ`, Line: 34, Col: 76}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</td><td class=\"px-6 py-4 text-sm text-gray-500\"><div class=\"max-w-md line-clamp-2\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(comment.Body)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/comment.templ`, Line: 37, Col: 81}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if comment.IsActive {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800\">Approved</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800\">Pending</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td class=\"px-6 py-4 whitespace-nowrap text-right text-sm font-medium\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/blog/comments/%d/edit", comment.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/comment.templ`, Line: 47, Col: 112}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"text-indigo-600 hover:text-indigo-900 mr-4\">Edit</a><form id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-comment-%d", comment.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/comment.templ`, Line: 48, Col: 87}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/blog/comments/%d/delete", comment.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/comment.templ`, Line: 48, Col: 171}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" method=\"POST\" class=\"inline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('Delete Comment', 'Are you sure you want to delete this comment?', 'delete-comment-%d')", comment.ID)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<button type=\"button\" class=\"text-red-600 hover:text-red-900\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('Delete Comment', 'Are you sure you want to delete this comment?', 'delete-comment-%d')", comment.ID)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">Delete</button></form></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</tbody></table></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Blog Comments").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CommentForm(comment *models.Comment, errors map[string]string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var11 == nil {
|
||||||
|
templ_7745c5c3_Var11 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var12 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"max-w-2xl mx-auto py-6\"><h1 class=\"text-2xl font-semibold text-gray-900 mb-6\">Edit Comment</h1><form action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/blog/comments/%d", comment.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/comment.templ`, Line: 64, Col: 92}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" method=\"POST\" class=\"space-y-6 bg-white p-6 rounded-lg shadow\"><div class=\"grid grid-cols-2 gap-4\"><div><label class=\"block text-sm font-medium text-gray-700\">User ID</label><p class=\"mt-1 p-2 bg-gray-50 border rounded text-sm text-gray-900\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", comment.UserID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/comment.templ`, Line: 68, Col: 127}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</p></div></div><div><label for=\"body\" class=\"block text-sm font-medium text-gray-700\">Comment Body</label> <textarea name=\"body\" id=\"body\" rows=\"5\" required class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(comment.Body)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/comment.templ`, Line: 75, Col: 169}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</textarea></div><div class=\"flex items-center\"><label class=\"flex items-center\"><input type=\"checkbox\" name=\"is_active\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if comment.IsActive {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " checked")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " class=\"rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"> <span class=\"ml-2 text-sm text-gray-700\">Approved / Active</span></label></div><div class=\"flex justify-end gap-3\"><a href=\"/admin/blog/comments\" class=\"bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50\">Cancel</a> <button type=\"submit\" class=\"bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700\">Save Changes</button></div></form></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Edit Comment").Render(templ.WithChildren(ctx, templ_7745c5c3_Var12), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
70
views/admin/blog/layout.templ
Normal file
70
views/admin/blog/layout.templ
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package blog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/views/admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Layout(title string) {
|
||||||
|
@admin.Layout(title) {
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="mb-6 border-b border-gray-200">
|
||||||
|
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
|
||||||
|
<a href="/admin/blog" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">Posts</a>
|
||||||
|
<a href="/admin/blog/categories" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">Categories</a>
|
||||||
|
<a href="/admin/blog/tags" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">Tags</a>
|
||||||
|
<a href="/admin/blog/comments" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">Comments</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
{ children... }
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function confirmDelete(title, text, formId) {
|
||||||
|
Swal.fire({
|
||||||
|
title: title || 'Are you sure?',
|
||||||
|
text: text || "You won't be able to revert this!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#3085d6',
|
||||||
|
cancelButtonColor: '#d33',
|
||||||
|
confirmButtonText: 'Yes, delete it!'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
document.getElementById(formId).submit();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugify(text) {
|
||||||
|
const trMap = {
|
||||||
|
'çÇ': 'c',
|
||||||
|
'ğĞ': 'g',
|
||||||
|
'şŞ': 's',
|
||||||
|
'üÜ': 'u',
|
||||||
|
'ıİ': 'i',
|
||||||
|
'öÖ': 'o'
|
||||||
|
};
|
||||||
|
for (let key in trMap) {
|
||||||
|
text = text.replace(new RegExp('[' + key + ']', 'g'), trMap[key]);
|
||||||
|
}
|
||||||
|
return text.toString().toLowerCase()
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/[^\w\-]+/g, '')
|
||||||
|
.replace(/\-\-+/g, '-')
|
||||||
|
.replace(/^-+/, '')
|
||||||
|
.replace(/-+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoSlug(sourceId, targetId) {
|
||||||
|
const source = document.getElementById(sourceId);
|
||||||
|
const target = document.getElementById(targetId);
|
||||||
|
if (!source || !target) return;
|
||||||
|
|
||||||
|
source.addEventListener('input', function() {
|
||||||
|
// Only auto-fill if the target is empty or was auto-filled (basic check)
|
||||||
|
// For now, let's just always suggest if it's a new entry (manual override still works)
|
||||||
|
target.value = slugify(source.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
70
views/admin/blog/layout_templ.go
Normal file
70
views/admin/blog/layout_templ.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package blog
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/views/admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Layout(title string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"px-4 py-6 sm:px-0\"><div class=\"mb-6 border-b border-gray-200\"><nav class=\"-mb-px flex space-x-8\" aria-label=\"Tabs\"><a href=\"/admin/blog\" class=\"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm\">Posts</a> <a href=\"/admin/blog/categories\" class=\"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm\">Categories</a> <a href=\"/admin/blog/tags\" class=\"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm\">Tags</a> <a href=\"/admin/blog/comments\" class=\"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm\">Comments</a></nav></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><script>\n function confirmDelete(title, text, formId) {\n Swal.fire({\n title: title || 'Are you sure?',\n text: text || \"You won't be able to revert this!\",\n icon: 'warning',\n showCancelButton: true,\n confirmButtonColor: '#3085d6',\n cancelButtonColor: '#d33',\n confirmButtonText: 'Yes, delete it!'\n }).then((result) => {\n if (result.isConfirmed) {\n document.getElementById(formId).submit();\n }\n })\n }\n\n function slugify(text) {\n const trMap = {\n 'çÇ': 'c',\n 'ğĞ': 'g',\n 'şŞ': 's',\n 'üÜ': 'u',\n 'ıİ': 'i',\n 'öÖ': 'o'\n };\n for (let key in trMap) {\n text = text.replace(new RegExp('[' + key + ']', 'g'), trMap[key]);\n }\n return text.toString().toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^\\w\\-]+/g, '')\n .replace(/\\-\\-+/g, '-')\n .replace(/^-+/, '')\n .replace(/-+$/, '');\n }\n\n function autoSlug(sourceId, targetId) {\n const source = document.getElementById(sourceId);\n const target = document.getElementById(targetId);\n if (!source || !target) return;\n\n source.addEventListener('input', function() {\n // Only auto-fill if the target is empty or was auto-filled (basic check)\n // For now, let's just always suggest if it's a new entry (manual override still works)\n target.value = slugify(source.value);\n });\n }\n </script>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = admin.Layout(title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
87
views/admin/blog/tag.templ
Normal file
87
views/admin/blog/tag.templ
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package blog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ TagList(items []models.Tag) {
|
||||||
|
@Layout("Blog Tags") {
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">Tags</h1>
|
||||||
|
<a href="/admin/blog/tags/new" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Add Tag</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tag</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Slug</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
for _, tag := range items {
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ fmt.Sprintf("%d", tag.ID) }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{ tag.Tag }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ tag.Slug }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/admin/blog/tags/%d/edit", tag.ID)) } class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</a>
|
||||||
|
<form id={ fmt.Sprintf("delete-tag-%d", tag.ID) } action={ templ.SafeURL(fmt.Sprintf("/admin/blog/tags/%d/delete", tag.ID)) } method="POST" class="inline">
|
||||||
|
<button type="button" class="text-red-600 hover:text-red-900" onclick={ templ.ComponentScript{ Call: fmt.Sprintf("confirmDelete('Delete Tag', 'Delete this tag and remove it from all posts?', 'delete-tag-%d')", tag.ID) } }>Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TagForm(tag *models.Tag, errors map[string]string) {
|
||||||
|
@Layout(ifElse(tag.ID == 0, "Create Tag", "Edit Tag")) {
|
||||||
|
<div class="max-w-2xl mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">
|
||||||
|
if tag.ID == 0 {
|
||||||
|
Create New Tag
|
||||||
|
} else {
|
||||||
|
Edit Tag: { tag.Tag }
|
||||||
|
}
|
||||||
|
</h1>
|
||||||
|
<form action={ templ.SafeURL(ifElse(tag.ID == 0, "/admin/blog/tags", fmt.Sprintf("/admin/blog/tags/%d", tag.ID))) } method="POST" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div>
|
||||||
|
<label for="tag" class="block text-sm font-medium text-gray-700">Tag Name*</label>
|
||||||
|
<input type="text" name="tag" id="tag" value={ tag.Tag } required
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
if errors["tag"] != "" {
|
||||||
|
<p class="mt-2 text-sm text-red-600">{ errors["tag"] }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="slug" class="block text-sm font-medium text-gray-700">Slug (leave empty to auto-generate)</label>
|
||||||
|
<input type="text" name="slug" id="slug" value={ tag.Slug }
|
||||||
|
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/blog/tags" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
|
||||||
|
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">
|
||||||
|
if tag.ID == 0 {
|
||||||
|
Create Tag
|
||||||
|
} else {
|
||||||
|
Save Changes
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
autoSlug('tag', 'slug');
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
316
views/admin/blog/tag_templ.go
Normal file
316
views/admin/blog/tag_templ.go
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package blog
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TagList(items []models.Tag) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex justify-between items-center mb-6\"><h1 class=\"text-2xl font-semibold text-gray-900\">Tags</h1><a href=\"/admin/blog/tags/new\" class=\"bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700\">Add Tag</a></div><div class=\"bg-white shadow overflow-hidden sm:rounded-lg\"><table class=\"min-w-full divide-y divide-gray-200\"><thead class=\"bg-gray-50\"><tr><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">ID</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Tag</th><th class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Slug</th><th class=\"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\">Actions</th></tr></thead> <tbody class=\"bg-white divide-y divide-gray-200\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, tag := range items {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<tr><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", tag.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 28, Col: 117}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(tag.Tag)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 29, Col: 111}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(tag.Slug)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 30, Col: 100}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</td><td class=\"px-6 py-4 whitespace-nowrap text-right text-sm font-medium\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/blog/tags/%d/edit", tag.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 32, Col: 104}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"text-indigo-600 hover:text-indigo-900 mr-4\">Edit</a><form id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-tag-%d", tag.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 33, Col: 79}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/blog/tags/%d/delete", tag.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 33, Col: 155}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" method=\"POST\" class=\"inline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('Delete Tag', 'Delete this tag and remove it from all posts?', 'delete-tag-%d')", tag.ID)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<button type=\"button\" class=\"text-red-600 hover:text-red-900\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('Delete Tag', 'Delete this tag and remove it from all posts?', 'delete-tag-%d')", tag.ID)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">Delete</button></form></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</tbody></table></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Blog Tags").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TagForm(tag *models.Tag, errors map[string]string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var10 == nil {
|
||||||
|
templ_7745c5c3_Var10 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var11 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"max-w-2xl mx-auto py-6\"><h1 class=\"text-2xl font-semibold text-gray-900 mb-6\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if tag.ID == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Create New Tag")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Edit Tag: ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(tag.Tag)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 52, Col: 39}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</h1><form action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(ifElse(tag.ID == 0, "/admin/blog/tags", fmt.Sprintf("/admin/blog/tags/%d", tag.ID))))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 55, Col: 125}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" method=\"POST\" class=\"space-y-6 bg-white p-6 rounded-lg shadow\"><div><label for=\"tag\" class=\"block text-sm font-medium text-gray-700\">Tag Name*</label> <input type=\"text\" name=\"tag\" id=\"tag\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(tag.Tag)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 58, Col: 74}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" required class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if errors["tag"] != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<p class=\"mt-2 text-sm text-red-600\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(errors["tag"])
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 61, Col: 76}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div><div><label for=\"slug\" class=\"block text-sm font-medium text-gray-700\">Slug (leave empty to auto-generate)</label> <input type=\"text\" name=\"slug\" id=\"slug\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 string
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(tag.Slug)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/blog/tag.templ`, Line: 67, Col: 77}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"></div><div class=\"flex justify-end gap-3\"><a href=\"/admin/blog/tags\" class=\"bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50\">Cancel</a> <button type=\"submit\" class=\"bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if tag.ID == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Create Tag")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Save Changes")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</button></div></form><script>\n autoSlug('tag', 'slug');\n </script></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout(ifElse(tag.ID == 0, "Create Tag", "Edit Tag")).Render(templ.WithChildren(ctx, templ_7745c5c3_Var11), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
13
views/admin/dashboard.templ
Normal file
13
views/admin/dashboard.templ
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
templ Dashboard() {
|
||||||
|
@Layout("Dashboard") {
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="h-96 rounded-lg border-4 border-dashed border-gray-200">
|
||||||
|
<div class="flex items-center justify-center h-full">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-400">Dashboard Content</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
58
views/admin/dashboard_templ.go
Normal file
58
views/admin/dashboard_templ.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package admin
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func Dashboard() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"px-4 py-6 sm:px-0\"><div class=\"h-96 rounded-lg border-4 border-dashed border-gray-200\"><div class=\"flex items-center justify-center h-full\"><h1 class=\"text-3xl font-bold text-gray-400\">Dashboard Content</h1></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Dashboard").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
54
views/admin/layout.templ
Normal file
54
views/admin/layout.templ
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
templ Layout(title string) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full bg-gray-100">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>{ title } - Admin Panel</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
|
<!-- Quill Editor -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.0/dist/quill.snow.css" rel="stylesheet" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.0/dist/quill.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="h-full" hx-boost="true">
|
||||||
|
<div class="min-h-full">
|
||||||
|
<nav class="bg-gray-800">
|
||||||
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex h-16 items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="text-white font-bold text-xl">Admin Panel</span>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<div class="ml-10 flex items-baseline space-x-4">
|
||||||
|
<a href="/admin/dashboard" class="bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium" aria-current="page">Dashboard</a>
|
||||||
|
<a href="/admin/users" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Users</a>
|
||||||
|
<a href="/admin/blog" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Blog Posts</a>
|
||||||
|
<a href="/admin/blog/categories" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Categories</a>
|
||||||
|
<a href="/admin/blog/tags" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Tags</a>
|
||||||
|
<a href="/admin/blog/comments" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Comments</a>
|
||||||
|
<a href="/admin/settings/whitelist" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Settings</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/logout" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
|
||||||
|
{ children... }
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
61
views/admin/layout_templ.go
Normal file
61
views/admin/layout_templ.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package admin
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func Layout(title string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"h-full bg-gray-100\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/layout.templ`, Line: 9, Col: 16}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Admin Panel</title><script src=\"https://cdn.tailwindcss.com\"></script><script src=\"https://unpkg.com/htmx.org@1.9.10\"></script><script src=\"https://cdn.jsdelivr.net/npm/sweetalert2@11\"></script><link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\"><!-- Quill Editor --><link href=\"https://cdn.jsdelivr.net/npm/quill@2.0.0/dist/quill.snow.css\" rel=\"stylesheet\"><script src=\"https://cdn.jsdelivr.net/npm/quill@2.0.0/dist/quill.js\"></script></head><body class=\"h-full\" hx-boost=\"true\"><div class=\"min-h-full\"><nav class=\"bg-gray-800\"><div class=\"mx-auto max-w-7xl px-4 sm:px-6 lg:px-8\"><div class=\"flex h-16 items-center justify-between\"><div class=\"flex items-center\"><div class=\"flex-shrink-0\"><span class=\"text-white font-bold text-xl\">Admin Panel</span></div><div class=\"hidden md:block\"><div class=\"ml-10 flex items-baseline space-x-4\"><a href=\"/admin/dashboard\" class=\"bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium\" aria-current=\"page\">Dashboard</a> <a href=\"/admin/users\" class=\"text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium\">Users</a> <a href=\"/admin/blog\" class=\"text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium\">Blog Posts</a> <a href=\"/admin/blog/categories\" class=\"text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium\">Categories</a> <a href=\"/admin/blog/tags\" class=\"text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium\">Tags</a> <a href=\"/admin/blog/comments\" class=\"text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium\">Comments</a> <a href=\"/admin/settings/whitelist\" class=\"text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium\">Settings</a></div></div></div><div><a href=\"/admin/logout\" class=\"text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium\">Logout</a></div></div></div></nav><main><div class=\"mx-auto max-w-7xl py-6 sm:px-6 lg:px-8\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></main></div></body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
37
views/admin/login.templ
Normal file
37
views/admin/login.templ
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import "gobeyhan/views/components"
|
||||||
|
|
||||||
|
templ Login() {
|
||||||
|
@Layout("Login") {
|
||||||
|
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
|
||||||
|
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
|
<h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">Sign in to your account</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
|
<form class="space-y-6" action="/admin/login" method="POST">
|
||||||
|
@components.Input(components.InputProps{
|
||||||
|
Label: "Email address",
|
||||||
|
Name: "email",
|
||||||
|
Type: "email",
|
||||||
|
Placeholder: "admin@example.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
@components.Input(components.InputProps{
|
||||||
|
Label: "Password",
|
||||||
|
Name: "password",
|
||||||
|
Type: "password",
|
||||||
|
})
|
||||||
|
|
||||||
|
<div>
|
||||||
|
@components.Button(components.ButtonProps{
|
||||||
|
Type: "submit",
|
||||||
|
Label: "Sign in",
|
||||||
|
})
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
92
views/admin/login_templ.go
Normal file
92
views/admin/login_templ.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package admin
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import "gobeyhan/views/components"
|
||||||
|
|
||||||
|
func Login() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex min-h-full flex-col justify-center px-6 py-12 lg:px-8\"><div class=\"sm:mx-auto sm:w-full sm:max-w-sm\"><h2 class=\"mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900\">Sign in to your account</h2></div><div class=\"mt-10 sm:mx-auto sm:w-full sm:max-w-sm\"><form class=\"space-y-6\" action=\"/admin/login\" method=\"POST\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = components.Input(components.InputProps{
|
||||||
|
Label: "Email address",
|
||||||
|
Name: "email",
|
||||||
|
Type: "email",
|
||||||
|
Placeholder: "admin@example.com",
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = components.Input(components.InputProps{
|
||||||
|
Label: "Password",
|
||||||
|
Name: "password",
|
||||||
|
Type: "password",
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = components.Button(components.ButtonProps{
|
||||||
|
Type: "submit",
|
||||||
|
Label: "Sign in",
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></form></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout("Login").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
292
views/admin/settings/settings.templ
Normal file
292
views/admin/settings/settings.templ
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/views/admin"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Layout(title string) {
|
||||||
|
@admin.Layout(title) {
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="mb-6 border-b border-gray-200">
|
||||||
|
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
|
||||||
|
<a href="/admin/settings/whitelist" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">CORS Whitelist</a>
|
||||||
|
<a href="/admin/settings/blacklist" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">CORS Blacklist</a>
|
||||||
|
<a href="/admin/settings/rate-limits" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">Rate Limits</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
{ children... }
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function confirmDelete(formId) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
text: "You won't be able to revert this!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#3085d6',
|
||||||
|
cancelButtonColor: '#d33',
|
||||||
|
confirmButtonText: 'Yes, delete it!'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
document.getElementById(formId).submit();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ WhitelistList(items []models.CorsWhitelist) {
|
||||||
|
@Layout("CORS Whitelist") {
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">CORS Whitelist</h1>
|
||||||
|
<a href="/admin/settings/whitelist/new" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Add Origin</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Origin</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Active</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
for _, item := range items {
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{ item.Origin }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ item.Description }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
if item.IsActive {
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span>
|
||||||
|
} else {
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Inactive</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/admin/settings/whitelist/%d/edit", item.ID)) } class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</a>
|
||||||
|
<form id={ fmt.Sprintf("delete-whitelist-%d", item.ID) } action={ templ.SafeURL(fmt.Sprintf("/admin/settings/whitelist/%d/delete", item.ID)) } method="POST" class="inline">
|
||||||
|
<button type="button" class="text-red-600 hover:text-red-900" onclick={ templ.ComponentScript{ Call: fmt.Sprintf("confirmDelete('delete-whitelist-%d')", item.ID) } }>Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ WhitelistCreate(errors map[string]string) {
|
||||||
|
@Layout("Add Whitelist Origin") {
|
||||||
|
<div class="max-w-2xl mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Add Allowed Origin</h1>
|
||||||
|
<form action="/admin/settings/whitelist" method="POST" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div>
|
||||||
|
<label for="origin" class="block text-sm font-medium text-gray-700">Origin (e.g. https://example.com)</label>
|
||||||
|
<input type="text" name="origin" id="origin" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
if errors["origin"] != "" {
|
||||||
|
<p class="mt-2 text-sm text-red-600">{ errors["origin"] }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||||
|
<input type="text" name="description" id="description" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/settings/whitelist" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
|
||||||
|
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ WhitelistEdit(item *models.CorsWhitelist, errors map[string]string) {
|
||||||
|
@Layout("Edit Whitelist Origin") {
|
||||||
|
<div class="max-w-2xl mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Allowed Origin</h1>
|
||||||
|
<form action={ templ.SafeURL(fmt.Sprintf("/admin/settings/whitelist/%d", item.ID)) } method="POST" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div>
|
||||||
|
<label for="origin" class="block text-sm font-medium text-gray-700">Origin (e.g. https://example.com)</label>
|
||||||
|
<input type="text" name="origin" id="origin" value={ item.Origin } class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
if errors["origin"] != "" {
|
||||||
|
<p class="mt-2 text-sm text-red-600">{ errors["origin"] }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||||
|
<input type="text" name="description" id="description" value={ item.Description } class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/settings/whitelist" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
|
||||||
|
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ BlacklistList(items []models.CorsBlacklist) {
|
||||||
|
@Layout("CORS Blacklist") {
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">CORS Blacklist</h1>
|
||||||
|
<a href="/admin/settings/blacklist/new" class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700">Block Origin</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Origin</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Reason</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Active</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
for _, item := range items {
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{ item.Origin }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ item.Reason }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
if item.IsActive {
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span>
|
||||||
|
} else {
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Inactive</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/admin/settings/blacklist/%d/edit", item.ID)) } class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</a>
|
||||||
|
<form id={ fmt.Sprintf("delete-blacklist-%d", item.ID) } action={ templ.SafeURL(fmt.Sprintf("/admin/settings/blacklist/%d/delete", item.ID)) } method="POST" class="inline">
|
||||||
|
<button type="button" class="text-red-600 hover:text-red-900" onclick={ templ.ComponentScript{ Call: fmt.Sprintf("confirmDelete('delete-blacklist-%d')", item.ID) } }>Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
templ BlacklistCreate(errors map[string]string) {
|
||||||
|
@Layout("Block Origin") {
|
||||||
|
<div class="max-w-2xl mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Block Origin</h1>
|
||||||
|
<form action="/admin/settings/blacklist" method="POST" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div>
|
||||||
|
<label for="origin" class="block text-sm font-medium text-gray-700">Origin (e.g. 192.168.1.1)</label>
|
||||||
|
<input type="text" name="origin" id="origin" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
if errors["origin"] != "" {
|
||||||
|
<p class="mt-2 text-sm text-red-600">{ errors["origin"] }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="reason" class="block text-sm font-medium text-gray-700">Reason</label>
|
||||||
|
<input type="text" name="reason" id="reason" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/settings/blacklist" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
|
||||||
|
<button type="submit" class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700">Block</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ BlacklistEdit(item *models.CorsBlacklist, errors map[string]string) {
|
||||||
|
@Layout("Edit Blocked Origin") {
|
||||||
|
<div class="max-w-2xl mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Blocked Origin</h1>
|
||||||
|
<form action={ templ.SafeURL(fmt.Sprintf("/admin/settings/blacklist/%d", item.ID)) } method="POST" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div>
|
||||||
|
<label for="origin" class="block text-sm font-medium text-gray-700">Origin (e.g. 192.168.1.1)</label>
|
||||||
|
<input type="text" name="origin" id="origin" value={ item.Origin } class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
if errors["origin"] != "" {
|
||||||
|
<p class="mt-2 text-sm text-red-600">{ errors["origin"] }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="reason" class="block text-sm font-medium text-gray-700">Reason</label>
|
||||||
|
<input type="text" name="reason" id="reason" value={ item.Reason } class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/settings/blacklist" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
|
||||||
|
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ RateLimitList(items []models.RateLimitSetting) {
|
||||||
|
@Layout("Rate Limits") {
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">Rate Limits</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Max Requests</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Window (s)</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
for _, item := range items {
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{ item.Name }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ item.Description }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ fmt.Sprintf("%d", item.MaxRequests) }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ fmt.Sprintf("%v", item.WindowSeconds) }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/admin/settings/rate-limits/%d/edit", item.ID)) } class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</a>
|
||||||
|
if item.Name != "api" {
|
||||||
|
<form id={ fmt.Sprintf("delete-ratelimit-%d", item.ID) } action={ templ.SafeURL(fmt.Sprintf("/admin/settings/rate-limits/%d/delete", item.ID)) } method="POST" class="inline">
|
||||||
|
<button type="button" class="text-red-600 hover:text-red-900" onclick={ templ.ComponentScript{ Call: fmt.Sprintf("confirmDelete('delete-ratelimit-%d')", item.ID) } }>Delete</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ RateLimitEdit(item *models.RateLimitSetting, errors map[string]string) {
|
||||||
|
@Layout("Edit Rate Limit") {
|
||||||
|
<div class="max-w-2xl mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Rate Limit: { item.Name }</h1>
|
||||||
|
<form action={ templ.SafeURL(fmt.Sprintf("/admin/settings/rate-limits/%d", item.ID)) } method="POST" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
<div>
|
||||||
|
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
|
||||||
|
<input type="text" name="description" id="description" value={ item.Description } class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="max_requests" class="block text-sm font-medium text-gray-700">Max Requests</label>
|
||||||
|
<input type="number" name="max_requests" id="max_requests" value={ fmt.Sprintf("%d", item.MaxRequests) } class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="window_seconds" class="block text-sm font-medium text-gray-700">Window (seconds)</label>
|
||||||
|
<input type="number" name="window_seconds" id="window_seconds" value={ fmt.Sprintf("%d", item.WindowSeconds) } class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/settings/rate-limits" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</a>
|
||||||
|
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
1025
views/admin/settings/settings_templ.go
Normal file
1025
views/admin/settings/settings_templ.go
Normal file
File diff suppressed because it is too large
Load Diff
21
views/admin/user/create.templ
Normal file
21
views/admin/user/create.templ
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/views/admin"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Create(roles []models.Role, errors map[string]string) {
|
||||||
|
@admin.Layout("Create User") {
|
||||||
|
<div class="max-w-2xl mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Create New User</h1>
|
||||||
|
@Form(FormProps{
|
||||||
|
User: models.User{},
|
||||||
|
Roles: roles,
|
||||||
|
Action: "/admin/users",
|
||||||
|
IsEdit: false,
|
||||||
|
Errors: errors,
|
||||||
|
})
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
77
views/admin/user/create_templ.go
Normal file
77
views/admin/user/create_templ.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package user
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"gobeyhan/views/admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Create(roles []models.Role, errors map[string]string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-2xl mx-auto py-6\"><h1 class=\"text-2xl font-semibold text-gray-900 mb-6\">Create New User</h1>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = Form(FormProps{
|
||||||
|
User: models.User{},
|
||||||
|
Roles: roles,
|
||||||
|
Action: "/admin/users",
|
||||||
|
IsEdit: false,
|
||||||
|
Errors: errors,
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = admin.Layout("Create User").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
22
views/admin/user/edit.templ
Normal file
22
views/admin/user/edit.templ
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/views/admin"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Edit(user models.User, roles []models.Role, errors map[string]string) {
|
||||||
|
@admin.Layout("Edit User") {
|
||||||
|
<div class="max-w-2xl mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit User</h1>
|
||||||
|
@Form(FormProps{
|
||||||
|
User: user,
|
||||||
|
Roles: roles,
|
||||||
|
Action: fmt.Sprintf("/admin/users/%d", user.ID),
|
||||||
|
IsEdit: true,
|
||||||
|
Errors: errors,
|
||||||
|
})
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
78
views/admin/user/edit_templ.go
Normal file
78
views/admin/user/edit_templ.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package user
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"gobeyhan/views/admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Edit(user models.User, roles []models.Role, errors map[string]string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-2xl mx-auto py-6\"><h1 class=\"text-2xl font-semibold text-gray-900 mb-6\">Edit User</h1>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = Form(FormProps{
|
||||||
|
User: user,
|
||||||
|
Roles: roles,
|
||||||
|
Action: fmt.Sprintf("/admin/users/%d", user.ID),
|
||||||
|
IsEdit: true,
|
||||||
|
Errors: errors,
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = admin.Layout("Edit User").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
77
views/admin/user/form.templ
Normal file
77
views/admin/user/form.templ
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"gobeyhan/views/components"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FormProps struct {
|
||||||
|
User models.User
|
||||||
|
Roles []models.Role
|
||||||
|
Action string
|
||||||
|
IsEdit bool
|
||||||
|
Errors map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Form(props FormProps) {
|
||||||
|
<form action={ templ.SafeURL(props.Action) } method="POST" class="space-y-6 bg-white p-6 rounded-lg shadow">
|
||||||
|
@components.Input(components.InputProps{
|
||||||
|
Label: "Username",
|
||||||
|
Name: "username",
|
||||||
|
Type: "text",
|
||||||
|
Value: props.User.UserName,
|
||||||
|
Error: props.Errors["username"],
|
||||||
|
})
|
||||||
|
|
||||||
|
@components.Input(components.InputProps{
|
||||||
|
Label: "Email",
|
||||||
|
Name: "email",
|
||||||
|
Type: "email",
|
||||||
|
Value: props.User.Email,
|
||||||
|
Error: props.Errors["email"],
|
||||||
|
})
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="role_id" class="block text-sm font-medium text-gray-700">Role</label>
|
||||||
|
<select id="role_id" name="role_id" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md border">
|
||||||
|
for _, role := range props.Roles {
|
||||||
|
<option value={ fmt.Sprintf("%d", role.ID) } selected?={ (len(props.User.Roles) > 0 && props.User.Roles[0].ID == role.ID) || (len(props.User.Roles) == 0 && role.Name == "user") }>{ role.Name }</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input id="email_verified" name="email_verified" type="checkbox" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" checked?={ props.User.IsEmailVerified() } />
|
||||||
|
<label for="email_verified" class="ml-2 block text-sm text-gray-900">Email Verified</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
if !props.IsEdit {
|
||||||
|
@components.Input(components.InputProps{
|
||||||
|
Label: "Password",
|
||||||
|
Name: "password",
|
||||||
|
Type: "password",
|
||||||
|
Error: props.Errors["password"],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Password (Leave blank to keep current)</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<a href="/admin/users" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Cancel</a>
|
||||||
|
@components.Button(components.ButtonProps{
|
||||||
|
Type: "submit",
|
||||||
|
Label: "Save",
|
||||||
|
Class: "w-auto",
|
||||||
|
})
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
179
views/admin/user/form_templ.go
Normal file
179
views/admin/user/form_templ.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package user
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"gobeyhan/views/components"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FormProps struct {
|
||||||
|
User models.User
|
||||||
|
Roles []models.Role
|
||||||
|
Action string
|
||||||
|
IsEdit bool
|
||||||
|
Errors map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Form(props FormProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(props.Action))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/user/form.templ`, Line: 18, Col: 43}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" method=\"POST\" class=\"space-y-6 bg-white p-6 rounded-lg shadow\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = components.Input(components.InputProps{
|
||||||
|
Label: "Username",
|
||||||
|
Name: "username",
|
||||||
|
Type: "text",
|
||||||
|
Value: props.User.UserName,
|
||||||
|
Error: props.Errors["username"],
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = components.Input(components.InputProps{
|
||||||
|
Label: "Email",
|
||||||
|
Name: "email",
|
||||||
|
Type: "email",
|
||||||
|
Value: props.User.Email,
|
||||||
|
Error: props.Errors["email"],
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div><label for=\"role_id\" class=\"block text-sm font-medium text-gray-700\">Role</label> <select id=\"role_id\" name=\"role_id\" class=\"mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md border\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, role := range props.Roles {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<option value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", role.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/user/form.templ`, Line: 39, Col: 62}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if (len(props.User.Roles) > 0 && props.User.Roles[0].ID == role.ID) || (len(props.User.Roles) == 0 && role.Name == "user") {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(role.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/user/form.templ`, Line: 39, Col: 210}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</option>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</select></div><div class=\"flex items-center\"><input id=\"email_verified\" name=\"email_verified\" type=\"checkbox\" class=\"h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if props.User.IsEmailVerified() {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " checked")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "> <label for=\"email_verified\" class=\"ml-2 block text-sm text-gray-900\">Email Verified</label></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if !props.IsEdit {
|
||||||
|
templ_7745c5c3_Err = components.Input(components.InputProps{
|
||||||
|
Label: "Password",
|
||||||
|
Name: "password",
|
||||||
|
Type: "password",
|
||||||
|
Error: props.Errors["password"],
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"mb-4\"><label for=\"password\" class=\"block text-sm font-medium text-gray-700 mb-1\">Password (Leave blank to keep current)</label> <input type=\"password\" name=\"password\" id=\"password\" class=\"block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"flex justify-end gap-3\"><a href=\"/admin/users\" class=\"bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500\">Cancel</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = components.Button(components.ButtonProps{
|
||||||
|
Type: "submit",
|
||||||
|
Label: "Save",
|
||||||
|
Class: "w-auto",
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></form>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
71
views/admin/user/list.templ
Normal file
71
views/admin/user/list.templ
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"gobeyhan/views/admin"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ List(users []models.User) {
|
||||||
|
@admin.Layout("User Management") {
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">Users</h1>
|
||||||
|
<a href="/admin/users/new" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700">Add User</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
for _, user := range users {
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ fmt.Sprintf("%d", user.ID) }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{ user.UserName }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{ user.Email }</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
if len(user.Roles) > 0 {
|
||||||
|
{ user.Roles[0].Name }
|
||||||
|
} else {
|
||||||
|
<span class="text-gray-400">-</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<a href={ templ.SafeURL(fmt.Sprintf("/admin/users/%d/edit", user.ID)) } class="text-indigo-600 hover:text-indigo-900 mr-4">Edit</a>
|
||||||
|
<form id={ fmt.Sprintf("delete-form-%d", user.ID) } action={ templ.SafeURL(fmt.Sprintf("/admin/users/%d/delete", user.ID)) } method="POST" class="inline">
|
||||||
|
<button type="button" class="text-red-600 hover:text-red-900" onclick={ templ.ComponentScript{ Call: fmt.Sprintf("confirmDelete(%d)", user.ID) } }>Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function confirmDelete(id) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
text: "You won't be able to revert this!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#3085d6',
|
||||||
|
cancelButtonColor: '#d33',
|
||||||
|
confirmButtonText: 'Yes, delete it!'
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
document.getElementById('delete-form-' + id).submit();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
189
views/admin/user/list_templ.go
Normal file
189
views/admin/user/list_templ.go
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package user
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gobeyhan/database/models"
|
||||||
|
"gobeyhan/views/admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func List(users []models.User) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"px-4 py-6 sm:px-0\"><div class=\"flex justify-between items-center mb-6\"><h1 class=\"text-2xl font-semibold text-gray-900\">Users</h1><a href=\"/admin/users/new\" class=\"bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700\">Add User</a></div><div class=\"bg-white shadow overflow-hidden sm:rounded-lg\"><table class=\"min-w-full divide-y divide-gray-200\"><thead class=\"bg-gray-50\"><tr><th scope=\"col\" class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">ID</th><th scope=\"col\" class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Name</th><th scope=\"col\" class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Email</th><th scope=\"col\" class=\"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider\">Role</th><th scope=\"col\" class=\"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider\">Actions</th></tr></thead> <tbody class=\"bg-white divide-y divide-gray-200\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, user := range users {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<tr><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", user.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/user/list.templ`, Line: 31, Col: 98}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.UserName)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/user/list.templ`, Line: 32, Col: 97}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/user/list.templ`, Line: 33, Col: 82}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-500\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(user.Roles) > 0 {
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(user.Roles[0].Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/user/list.templ`, Line: 36, Col: 60}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"text-gray-400\">-</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</td><td class=\"px-6 py-4 whitespace-nowrap text-right text-sm font-medium\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/users/%d/edit", user.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/user/list.templ`, Line: 42, Col: 78}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"text-indigo-600 hover:text-indigo-900 mr-4\">Edit</a><form id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("delete-form-%d", user.ID))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/user/list.templ`, Line: 43, Col: 58}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" action=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/admin/users/%d/delete", user.ID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/admin/user/list.templ`, Line: 43, Col: 131}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" method=\"POST\" class=\"inline\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("confirmDelete(%d)", user.ID)})
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<button type=\"button\" class=\"text-red-600 hover:text-red-900\" onclick=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("confirmDelete(%d)", user.ID)}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10.Call)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">Delete</button></form></td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</tbody></table></div></div><script>\n function confirmDelete(id) {\n Swal.fire({\n title: 'Are you sure?',\n text: \"You won't be able to revert this!\",\n icon: 'warning',\n showCancelButton: true,\n confirmButtonColor: '#3085d6',\n cancelButtonColor: '#d33',\n confirmButtonText: 'Yes, delete it!'\n }).then((result) => {\n if (result.isConfirmed) {\n document.getElementById('delete-form-' + id).submit();\n }\n })\n }\n </script>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = admin.Layout("User Management").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
16
views/components/button.templ
Normal file
16
views/components/button.templ
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
type ButtonProps struct {
|
||||||
|
Type string // submit, button, reset
|
||||||
|
Label string
|
||||||
|
Class string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Button(props ButtonProps) {
|
||||||
|
<button
|
||||||
|
type={ props.Type }
|
||||||
|
class={ "flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 " + props.Class }
|
||||||
|
>
|
||||||
|
{ props.Label }
|
||||||
|
</button>
|
||||||
|
}
|
||||||
90
views/components/button_templ.go
Normal file
90
views/components/button_templ.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package components
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type ButtonProps struct {
|
||||||
|
Type string // submit, button, reset
|
||||||
|
Label string
|
||||||
|
Class string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Button(props ButtonProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var2 = []any{"flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 " + props.Class}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<button type=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(props.Type)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/button.templ`, Line: 11, Col: 25}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/button.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/button.templ`, Line: 14, Col: 21}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
27
views/components/input.templ
Normal file
27
views/components/input.templ
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
type InputProps struct {
|
||||||
|
Label string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Placeholder string
|
||||||
|
Value string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Input(props InputProps) {
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for={ props.Name } class="block text-sm font-medium text-gray-700 mb-1">{ props.Label }</label>
|
||||||
|
<input
|
||||||
|
type={ props.Type }
|
||||||
|
name={ props.Name }
|
||||||
|
id={ props.Name }
|
||||||
|
placeholder={ props.Placeholder }
|
||||||
|
value={ props.Value }
|
||||||
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border"
|
||||||
|
/>
|
||||||
|
if props.Error != "" {
|
||||||
|
<p class="mt-1 text-sm text-red-600">{ props.Error }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
163
views/components/input_templ.go
Normal file
163
views/components/input_templ.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.977
|
||||||
|
package components
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type InputProps struct {
|
||||||
|
Label string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Placeholder string
|
||||||
|
Value string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Input(props InputProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"mb-4\"><label for=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(props.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/input.templ`, Line: 14, Col: 31}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"block text-sm font-medium text-gray-700 mb-1\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/input.templ`, Line: 14, Col: 100}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</label> <input type=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.Type)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/input.templ`, Line: 16, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" name=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/input.templ`, Line: 17, Col: 29}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/input.templ`, Line: 18, Col: 27}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" placeholder=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(props.Placeholder)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/input.templ`, Line: 19, Col: 43}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(props.Value)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/input.templ`, Line: 20, Col: 31}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2 border\"> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if props.Error != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"mt-1 text-sm text-red-600\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(props.Error)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/input.templ`, Line: 24, Col: 62}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
0
yapilacak.txt
Normal file
0
yapilacak.txt
Normal file
Reference in New Issue
Block a user