first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:43:40 +03:00
commit f34e54c5a5
100 changed files with 27342 additions and 0 deletions

58
.air.toml Normal file
View File

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

98
.env Normal file
View 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
View 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
View 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ıı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ıı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
View 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)

View 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
}

View 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
}

View 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})
}

View 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"})
}

View 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"})
}

View 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"})
}

View 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
}

View 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)
}

View 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
}

View 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)
}

View 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"})
}

View 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,
})
}

View 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"})
}

View 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"})
}

View 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"})
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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()
}
}

View 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()
}
}

View 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()
}
}

View 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
View 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
View 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)
}
}
}
}

View 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"})
}

View 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")
}

View 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
}

View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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()
}

3
dev.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd admin-panel && yarn dev & air && fg

3034
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

3010
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

1942
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

78
go.mod Normal file
View 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
View 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=

View 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")
}

View 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)
}

View 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")
}

View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View File

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

1505
seed_force.log Normal file

File diff suppressed because it is too large Load Diff

1499
seed_run.log Normal file

File diff suppressed because it is too large Load Diff

1235
server.log Normal file

File diff suppressed because it is too large Load Diff

1
update_log.txt Normal file
View File

@@ -0,0 +1 @@
# This is a dummy file to update user.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

403
views/admin/blog/blog.templ Normal file
View 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
}

View 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

View 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
}

View 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

View 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>
}
}

View 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

View 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>
}
}

View 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

View 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>
}
}

View 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

View 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>
}
}

View 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
View 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>
}

View 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
View 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>
}
}

View 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

View 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>
}
}

File diff suppressed because it is too large Load Diff

View 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>
}
}

View 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

View 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>
}
}

View 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

View 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>
}

View 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

View 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>
}
}

View 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

View 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>
}

View 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

View 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>
}

View 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
View File