first commit
This commit is contained in:
52
controllers/admin_cart_controller.go
Normal file
52
controllers/admin_cart_controller.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
dbConfig "ares/database/config"
|
||||
"ares/database/models"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// AdminContentCarts handles rendering the Carts list in the admin panel
|
||||
func AdminContentCarts(c fiber.Ctx) error {
|
||||
var carts []models.Cart
|
||||
|
||||
// Preload the User to display who owns the cart
|
||||
// Preload Items to show the item count
|
||||
query := dbConfig.DB.Model(&models.Cart{}).Preload("Items")
|
||||
|
||||
// Filter by User ID if a search is provided (basic example, finding user by ID)
|
||||
search := c.Query("search")
|
||||
if search != "" {
|
||||
if userID, err := strconv.Atoi(search); err == nil {
|
||||
query = query.Where("user_id = ?", userID)
|
||||
}
|
||||
}
|
||||
|
||||
query.Order("updated_at desc").Find(&carts)
|
||||
|
||||
data := fiber.Map{
|
||||
"Carts": carts,
|
||||
"Search": search,
|
||||
}
|
||||
|
||||
// Render partial if requested via HTMX
|
||||
if c.Get("HX-Request") == "true" {
|
||||
return c.Render("admin/partials/carts", data)
|
||||
}
|
||||
|
||||
// Otherwise render full layout
|
||||
return c.Render("admin/partials/carts", data, "admin/layout")
|
||||
}
|
||||
|
||||
// AdminCartDelete handles deleting a cart (useful for clearing abandoned carts)
|
||||
func AdminCartDelete(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
|
||||
if err := dbConfig.DB.Unscoped().Where("id = ?", id).Delete(&models.Cart{}).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/carts?error=Silme+başarısız")
|
||||
}
|
||||
|
||||
return c.Redirect().To("/admin/content/carts?deleted=true&success=Sepet+silindi")
|
||||
}
|
||||
1641
controllers/admin_controller.go
Normal file
1641
controllers/admin_controller.go
Normal file
File diff suppressed because it is too large
Load Diff
619
controllers/admin_product_controller.go
Normal file
619
controllers/admin_product_controller.go
Normal file
@@ -0,0 +1,619 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
dbConfig "ares/database/config"
|
||||
"ares/database/models"
|
||||
utils "ares/pkg/utis"
|
||||
"ares/services"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// AdminContentProducts renders the products partial
|
||||
func AdminContentProducts(c fiber.Ctx) error {
|
||||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||||
limit := 20
|
||||
offset := (page - 1) * limit
|
||||
search := c.Query("search", "")
|
||||
showDeleted := c.Query("deleted") == "true"
|
||||
|
||||
var products []models.Product
|
||||
var total int64
|
||||
|
||||
query := dbConfig.DB.Model(&models.Product{})
|
||||
if showDeleted {
|
||||
query = query.Unscoped().Where("deleted_at IS NOT NULL")
|
||||
}
|
||||
if search != "" {
|
||||
query = query.Where("title LIKE ? OR slug LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
query.Count(&total)
|
||||
query.Preload("Categories").Preload("Tags").Order("created_at desc").Limit(limit).Offset(offset).Find(&products)
|
||||
|
||||
imageMap := make(map[uint]string)
|
||||
for _, p := range products {
|
||||
if p.Images != "" {
|
||||
imgs := parseImagesField(p.Images)
|
||||
if len(imgs) > 0 {
|
||||
imageMap[p.ID] = imgs[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalPages := int(math.Ceil(float64(total) / float64(limit)))
|
||||
|
||||
data := fiber.Map{
|
||||
"Products": products,
|
||||
"ImageMap": imageMap,
|
||||
"Page": page,
|
||||
"TotalPages": totalPages,
|
||||
"NextPage": page + 1,
|
||||
"PrevPage": page - 1,
|
||||
"Search": search,
|
||||
"ShowDeleted": showDeleted,
|
||||
}
|
||||
|
||||
if c.Get("HX-Request") == "true" {
|
||||
return c.Render("admin/partials/products", data)
|
||||
}
|
||||
return c.Render("admin/partials/products", data, "admin/layout")
|
||||
}
|
||||
|
||||
// AdminProductNew renders create form
|
||||
func AdminProductNew(c fiber.Ctx) error {
|
||||
var cats []models.ProductCategory
|
||||
var tags []models.ProductTag
|
||||
dbConfig.DB.Order("title asc").Find(&cats)
|
||||
dbConfig.DB.Order("name asc").Find(&tags)
|
||||
return c.Render("admin/pages/product_form", fiber.Map{"IsEdit": false, "Categories": cats, "Tags": tags, "FirstImage": ""}, "admin/layout")
|
||||
}
|
||||
|
||||
// AdminProductCreate handles creation
|
||||
func AdminProductCreate(c fiber.Ctx) error {
|
||||
title := c.FormValue("title")
|
||||
if title == "" {
|
||||
return c.Redirect().To("/admin/content/products?error=Başlık+gerekli")
|
||||
}
|
||||
product := models.Product{Title: title}
|
||||
product.Content = c.FormValue("content")
|
||||
|
||||
// Slug handling
|
||||
rawSlug := c.FormValue("slug")
|
||||
if rawSlug == "" {
|
||||
rawSlug = utils.Slugify(product.Title)
|
||||
} else {
|
||||
rawSlug = utils.Slugify(rawSlug)
|
||||
}
|
||||
attempt := rawSlug
|
||||
i := 1
|
||||
for {
|
||||
var existing models.Product
|
||||
err := dbConfig.DB.Unscoped().Where("slug = ?", attempt).First(&existing).Error
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
attempt = rawSlug + "-" + strconv.Itoa(i)
|
||||
i++
|
||||
}
|
||||
product.Slug = attempt
|
||||
|
||||
// Image Upload
|
||||
priceStr := c.FormValue("price")
|
||||
price, _ := strconv.ParseFloat(priceStr, 64)
|
||||
width, _ := strconv.Atoi(c.FormValue("width"))
|
||||
height, _ := strconv.Atoi(c.FormValue("height"))
|
||||
quality, _ := strconv.Atoi(c.FormValue("quality"))
|
||||
format := c.FormValue("format")
|
||||
if format == "" {
|
||||
format = "avif"
|
||||
}
|
||||
// DB fields
|
||||
product.Price = price
|
||||
product.Format = format
|
||||
product.Width = width
|
||||
product.Height = height
|
||||
product.Quality = quality
|
||||
|
||||
// Use processAndSaveImage for main product image (for simplicity we store 1 image mapped as JSON array or just single string per parseImagesField)
|
||||
imagePath, err := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
|
||||
Width: width,
|
||||
Height: height,
|
||||
Quality: quality,
|
||||
Format: format,
|
||||
Folder: "products",
|
||||
})
|
||||
if err == nil && imagePath != "" {
|
||||
// Tek resim olarak string kaydet
|
||||
product.Images = imagePath
|
||||
}
|
||||
|
||||
if err := dbConfig.DB.Create(&product).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/products?error=Oluşturma+başarısız")
|
||||
}
|
||||
|
||||
// Handline Relations (Categories & Tags)
|
||||
form, err := c.MultipartForm()
|
||||
|
||||
if err == nil && form != nil {
|
||||
catIDs := form.Value["categories"]
|
||||
for _, catIDStr := range catIDs {
|
||||
catID, _ := strconv.Atoi(catIDStr)
|
||||
if catID > 0 {
|
||||
var cat models.ProductCategory
|
||||
if err := dbConfig.DB.First(&cat, catID).Error; err == nil {
|
||||
dbConfig.DB.Model(&product).Association("Categories").Append(&cat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tagIDs := form.Value["tags"]
|
||||
for _, tagIDStr := range tagIDs {
|
||||
tagID, _ := strconv.Atoi(tagIDStr)
|
||||
if tagID > 0 {
|
||||
var tag models.ProductTag
|
||||
if err := dbConfig.DB.First(&tag, tagID).Error; err == nil {
|
||||
dbConfig.DB.Model(&product).Association("Tags").Append(&tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if form data is not multipart, try basic FormValue array
|
||||
// Though Fiber usually parses multiple values when we get them explicitly or we can just parse the body
|
||||
}
|
||||
|
||||
return c.Redirect().To("/admin/content/products?success=Ürün+oluşturuldu")
|
||||
}
|
||||
|
||||
// AdminProductEdit renders the edit hero form
|
||||
func AdminProductEdit(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
var product models.Product
|
||||
if err := dbConfig.DB.Preload("Categories").Preload("Tags").First(&product, id).Error; err != nil {
|
||||
return c.Status(fiber.StatusNotFound).SendString("Ürün bulunamadı")
|
||||
}
|
||||
|
||||
var cats []models.ProductCategory
|
||||
var tags []models.ProductTag
|
||||
dbConfig.DB.Order("title asc").Find(&cats)
|
||||
dbConfig.DB.Order("name asc").Find(&tags)
|
||||
|
||||
firstImage := ""
|
||||
if product.Images != "" {
|
||||
imgs := parseImagesField(product.Images)
|
||||
if len(imgs) > 0 {
|
||||
firstImage = imgs[0]
|
||||
}
|
||||
}
|
||||
|
||||
return c.Render("admin/pages/product_form", fiber.Map{
|
||||
"IsEdit": true,
|
||||
"Product": product,
|
||||
"Categories": cats,
|
||||
"Tags": tags,
|
||||
"FirstImage": firstImage,
|
||||
}, "admin/layout")
|
||||
}
|
||||
|
||||
// AdminProductUpdate handles product update
|
||||
func AdminProductUpdate(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
var product models.Product
|
||||
if err := dbConfig.DB.First(&product, id).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/products?error=Ürün+bulunamadı")
|
||||
}
|
||||
|
||||
product.Title = c.FormValue("title")
|
||||
product.Content = c.FormValue("content")
|
||||
|
||||
// Slug update
|
||||
rawSlug := c.FormValue("slug")
|
||||
if rawSlug != "" && rawSlug != product.Slug {
|
||||
rawSlug = utils.Slugify(rawSlug)
|
||||
attempt := rawSlug
|
||||
i := 1
|
||||
for {
|
||||
var existing models.Product
|
||||
err := dbConfig.DB.Unscoped().Where("slug = ? AND id != ?", attempt, product.ID).First(&existing).Error
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
attempt = rawSlug + "-" + strconv.Itoa(i)
|
||||
i++
|
||||
}
|
||||
product.Slug = attempt
|
||||
}
|
||||
|
||||
// Image Upload
|
||||
priceStr := c.FormValue("price")
|
||||
price, _ := strconv.ParseFloat(priceStr, 64)
|
||||
width, _ := strconv.Atoi(c.FormValue("width"))
|
||||
height, _ := strconv.Atoi(c.FormValue("height"))
|
||||
quality, _ := strconv.Atoi(c.FormValue("quality"))
|
||||
format := c.FormValue("format")
|
||||
if format == "" {
|
||||
format = "avif"
|
||||
}
|
||||
// DB fields
|
||||
product.Price = price
|
||||
product.Format = format
|
||||
product.Width = width
|
||||
product.Height = height
|
||||
product.Quality = quality
|
||||
|
||||
imagePath, err := services.ProcessAndSaveImage(c, "image", services.ImageOptions{
|
||||
Width: width,
|
||||
Height: height,
|
||||
Quality: quality,
|
||||
Format: format,
|
||||
Folder: "products",
|
||||
})
|
||||
if err == nil && imagePath != "" {
|
||||
product.Images = imagePath
|
||||
}
|
||||
|
||||
if err := dbConfig.DB.Save(&product).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/products?error=Güncelleme+başarısız")
|
||||
}
|
||||
|
||||
// Handline Relations (Categories & Tags)
|
||||
dbConfig.DB.Model(&product).Association("Categories").Clear()
|
||||
dbConfig.DB.Model(&product).Association("Tags").Clear()
|
||||
|
||||
form, err := c.MultipartForm()
|
||||
|
||||
if err == nil && form != nil {
|
||||
catIDs := form.Value["categories"]
|
||||
for _, catIDStr := range catIDs {
|
||||
catID, _ := strconv.Atoi(catIDStr)
|
||||
if catID > 0 {
|
||||
var cat models.ProductCategory
|
||||
if err := dbConfig.DB.First(&cat, catID).Error; err == nil {
|
||||
dbConfig.DB.Model(&product).Association("Categories").Append(&cat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tagIDs := form.Value["tags"]
|
||||
for _, tagIDStr := range tagIDs {
|
||||
tagID, _ := strconv.Atoi(tagIDStr)
|
||||
if tagID > 0 {
|
||||
var tag models.ProductTag
|
||||
if err := dbConfig.DB.First(&tag, tagID).Error; err == nil {
|
||||
dbConfig.DB.Model(&product).Association("Tags").Append(&tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.Redirect().To("/admin/content/products?success=Ürün+güncellendi")
|
||||
}
|
||||
|
||||
// AdminProductDelete handles soft delete
|
||||
func AdminProductDelete(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
if err := dbConfig.DB.Delete(&models.Product{}, id).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/products?error=Silme+hatası")
|
||||
}
|
||||
return c.Redirect().To("/admin/content/products?success=Ürün+silindi")
|
||||
}
|
||||
|
||||
// AdminProductRestore handles restore
|
||||
func AdminProductRestore(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
if err := dbConfig.DB.Unscoped().Model(&models.Product{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/products?error=Geri+yükleme+hatası")
|
||||
}
|
||||
return c.Redirect().To("/admin/content/products?deleted=true&success=Ürün+geri+yüklendi")
|
||||
}
|
||||
|
||||
// --- ProductCategory Management ---
|
||||
|
||||
func AdminContentProductCategories(c fiber.Ctx) error {
|
||||
var categories []models.ProductCategory
|
||||
showDeleted := c.Query("deleted") == "true"
|
||||
query := dbConfig.DB.Model(&models.ProductCategory{})
|
||||
if showDeleted {
|
||||
query = query.Unscoped().Where("deleted_at IS NOT NULL")
|
||||
}
|
||||
query.Preload("Parent").Order("title asc").Find(&categories)
|
||||
|
||||
data := fiber.Map{
|
||||
"Categories": categories,
|
||||
"ShowDeleted": showDeleted,
|
||||
}
|
||||
|
||||
if c.Get("HX-Request") == "true" {
|
||||
return c.Render("admin/partials/product_categories", data)
|
||||
}
|
||||
return c.Render("admin/partials/product_categories", data, "admin/layout")
|
||||
}
|
||||
|
||||
func AdminProductCategoryNew(c fiber.Ctx) error {
|
||||
var parents []models.ProductCategory
|
||||
dbConfig.DB.Order("title asc").Find(&parents)
|
||||
return c.Render("admin/pages/product_category_form", fiber.Map{
|
||||
"IsEdit": false,
|
||||
"Parents": parents,
|
||||
}, "admin/layout")
|
||||
}
|
||||
|
||||
func AdminProductCategoryCreate(c fiber.Ctx) error {
|
||||
cat := new(models.ProductCategory)
|
||||
if err := c.Bind().Body(cat); err != nil {
|
||||
return c.Redirect().To("/admin/content/product-categories?error=Geçersiz+istek")
|
||||
}
|
||||
|
||||
if cat.Title == "" {
|
||||
return c.Redirect().To("/admin/content/product-categories?error=Başlık+zorunlu")
|
||||
}
|
||||
|
||||
rawSlug := c.FormValue("slug")
|
||||
if rawSlug == "" {
|
||||
rawSlug = utils.Slugify(cat.Title)
|
||||
} else {
|
||||
rawSlug = utils.Slugify(rawSlug)
|
||||
}
|
||||
|
||||
attempt := rawSlug
|
||||
i := 1
|
||||
for {
|
||||
var existing models.ProductCategory
|
||||
err := dbConfig.DB.Unscoped().Where("slug = ?", attempt).First(&existing).Error
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
attempt = rawSlug + "-" + strconv.Itoa(i)
|
||||
i++
|
||||
}
|
||||
cat.Slug = attempt
|
||||
|
||||
// Handle ParentID
|
||||
parentIDStr := c.FormValue("parent_id")
|
||||
if parentIDStr != "" {
|
||||
if pid, err := strconv.ParseUint(parentIDStr, 10, 32); err == nil && pid > 0 {
|
||||
pidUint := uint(pid)
|
||||
cat.ParentID = &pidUint
|
||||
}
|
||||
} else {
|
||||
cat.ParentID = nil
|
||||
}
|
||||
|
||||
if err := dbConfig.DB.Create(cat).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-categories?error=Oluşturma+başarısız")
|
||||
}
|
||||
return c.Redirect().To("/admin/content/product-categories?success=Kategori+oluşturuldu")
|
||||
}
|
||||
|
||||
func AdminProductCategoryEdit(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
var cat models.ProductCategory
|
||||
if err := dbConfig.DB.First(&cat, id).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-categories?error=Kategori+bulunamadı")
|
||||
}
|
||||
|
||||
var parents []models.ProductCategory
|
||||
dbConfig.DB.Where("id != ?", id).Order("title asc").Find(&parents)
|
||||
|
||||
return c.Render("admin/pages/product_category_form", fiber.Map{
|
||||
"IsEdit": true,
|
||||
"Category": cat,
|
||||
"Parents": parents,
|
||||
}, "admin/layout")
|
||||
}
|
||||
|
||||
func AdminProductCategoryUpdate(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
var cat models.ProductCategory
|
||||
if err := dbConfig.DB.First(&cat, id).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-categories?error=Kategori+bulunamadı")
|
||||
}
|
||||
|
||||
cat.Title = c.FormValue("title")
|
||||
cat.Description = c.FormValue("description")
|
||||
cat.Keywords = c.FormValue("keywords")
|
||||
|
||||
rawSlug := c.FormValue("slug")
|
||||
if rawSlug != "" && rawSlug != cat.Slug {
|
||||
rawSlug = utils.Slugify(rawSlug)
|
||||
attempt := rawSlug
|
||||
i := 1
|
||||
for {
|
||||
var existing models.ProductCategory
|
||||
err := dbConfig.DB.Unscoped().Where("slug = ? AND id != ?", attempt, cat.ID).First(&existing).Error
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
attempt = rawSlug + "-" + strconv.Itoa(i)
|
||||
i++
|
||||
}
|
||||
cat.Slug = attempt
|
||||
}
|
||||
|
||||
parentIDStr := c.FormValue("parent_id")
|
||||
if parentIDStr != "" {
|
||||
if pid, err := strconv.ParseUint(parentIDStr, 10, 32); err == nil && pid > 0 {
|
||||
pidUint := uint(pid)
|
||||
if pidUint != cat.ID {
|
||||
cat.ParentID = &pidUint
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cat.ParentID = nil
|
||||
}
|
||||
|
||||
if err := dbConfig.DB.Save(&cat).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-categories?error=Güncelleme+başarısız")
|
||||
}
|
||||
return c.Redirect().To("/admin/content/product-categories?success=Kategori+güncellendi")
|
||||
}
|
||||
|
||||
func AdminProductCategoryDelete(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
if err := dbConfig.DB.Delete(&models.ProductCategory{}, id).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-categories?error=Silme+başarısız")
|
||||
}
|
||||
return c.Redirect().To("/admin/content/product-categories?success=Kategori+silindi")
|
||||
}
|
||||
|
||||
func AdminProductCategoryRestore(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
if err := dbConfig.DB.Unscoped().Model(&models.ProductCategory{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-categories?error=Geri+yükleme+başarısız")
|
||||
}
|
||||
return c.Redirect().To("/admin/content/product-categories?deleted=true&success=Kategori+geri+yüklendi")
|
||||
}
|
||||
|
||||
// --- ProductTag Management ---
|
||||
|
||||
func AdminContentProductTags(c fiber.Ctx) error {
|
||||
var tags []models.ProductTag
|
||||
showDeleted := c.Query("deleted") == "true"
|
||||
query := dbConfig.DB.Model(&models.ProductTag{})
|
||||
if showDeleted {
|
||||
query = query.Unscoped().Where("deleted_at IS NOT NULL")
|
||||
}
|
||||
query.Order("name asc").Find(&tags)
|
||||
|
||||
data := fiber.Map{
|
||||
"Tags": tags,
|
||||
"ShowDeleted": showDeleted,
|
||||
}
|
||||
|
||||
if c.Get("HX-Request") == "true" {
|
||||
return c.Render("admin/partials/product_tags", data)
|
||||
}
|
||||
return c.Render("admin/partials/product_tags", data, "admin/layout")
|
||||
}
|
||||
|
||||
func AdminProductTagNew(c fiber.Ctx) error {
|
||||
return c.Render("admin/pages/product_tag_form", fiber.Map{
|
||||
"IsEdit": false,
|
||||
}, "admin/layout")
|
||||
}
|
||||
|
||||
func AdminProductTagCreate(c fiber.Ctx) error {
|
||||
tag := new(models.ProductTag)
|
||||
if err := c.Bind().Body(tag); err != nil {
|
||||
return c.Redirect().To("/admin/content/product-tags?error=Geçersiz+istek")
|
||||
}
|
||||
if tag.Name == "" {
|
||||
return c.Redirect().To("/admin/content/product-tags?error=İsim+zorunlu")
|
||||
}
|
||||
if err := dbConfig.DB.Create(tag).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-tags?error=Oluşturma+başarısız")
|
||||
}
|
||||
return c.Redirect().To("/admin/content/product-tags?success=Etiket+oluşturuldu")
|
||||
}
|
||||
|
||||
func AdminProductTagEdit(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
var tag models.ProductTag
|
||||
if err := dbConfig.DB.First(&tag, id).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-tags?error=Etiket+bulunamadı")
|
||||
}
|
||||
return c.Render("admin/pages/product_tag_form", fiber.Map{
|
||||
"IsEdit": true,
|
||||
"Tag": tag,
|
||||
}, "admin/layout")
|
||||
}
|
||||
|
||||
func AdminProductTagUpdate(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
var tag models.ProductTag
|
||||
if err := dbConfig.DB.First(&tag, id).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-tags?error=Etiket+bulunamadı")
|
||||
}
|
||||
|
||||
tag.Name = c.FormValue("name")
|
||||
|
||||
if err := dbConfig.DB.Save(&tag).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-tags?error=Güncelleme+başarısız")
|
||||
}
|
||||
return c.Redirect().To("/admin/content/product-tags?success=Etiket+güncellendi")
|
||||
}
|
||||
|
||||
func AdminProductTagDelete(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
if err := dbConfig.DB.Delete(&models.ProductTag{}, id).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-tags?error=Silme+başarısız")
|
||||
}
|
||||
return c.Redirect().To("/admin/content/product-tags?success=Etiket+silindi")
|
||||
}
|
||||
|
||||
func AdminProductTagRestore(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
if err := dbConfig.DB.Unscoped().Model(&models.ProductTag{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-tags?error=Geri+yükleme+başarısız")
|
||||
}
|
||||
return c.Redirect().To("/admin/content/product-tags?deleted=true&success=Etiket+geri+yüklendi")
|
||||
}
|
||||
|
||||
// AdminContentProductComments handles rendering the Product Comments list in the admin panel
|
||||
func AdminContentProductComments(c fiber.Ctx) error {
|
||||
var comments []models.ProductComment
|
||||
|
||||
// Preload the User and Product for display (Wait, user and product relationships are missing in model temporarily)
|
||||
// We'll just list them out manually
|
||||
query := dbConfig.DB.Model(&models.ProductComment{})
|
||||
|
||||
// Optional filtering by specific product via query string
|
||||
productID := c.Query("product_id")
|
||||
if productID != "" {
|
||||
if pid, err := strconv.Atoi(productID); err == nil {
|
||||
query = query.Where("product_id = ?", pid)
|
||||
}
|
||||
}
|
||||
|
||||
query.Order("created_at desc").Find(&comments)
|
||||
|
||||
data := fiber.Map{
|
||||
"ProductComments": comments,
|
||||
"ProductID": productID,
|
||||
}
|
||||
|
||||
if c.Get("HX-Request") == "true" {
|
||||
return c.Render("admin/partials/product_comments", data)
|
||||
}
|
||||
|
||||
return c.Render("admin/partials/product_comments", data, "admin/layout")
|
||||
}
|
||||
|
||||
// AdminProductCommentDelete handles hard deleting a product comment
|
||||
func AdminProductCommentDelete(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
if err := dbConfig.DB.Unscoped().Where("id = ?", id).Delete(&models.ProductComment{}).Error; err != nil {
|
||||
return c.Redirect().To("/admin/content/product-comments?error=Yorum+silinemedi")
|
||||
}
|
||||
|
||||
return c.Redirect().To("/admin/content/product-comments?deleted=true&success=Yorum+silindi")
|
||||
}
|
||||
|
||||
// AdminContentProductCategoryViews handles rendering the Product Category Views list
|
||||
func AdminContentProductCategoryViews(c fiber.Ctx) error {
|
||||
var views []models.ProductCategoryView
|
||||
|
||||
query := dbConfig.DB.Model(&models.ProductCategoryView{})
|
||||
|
||||
// Filter by Category ID
|
||||
categoryID := c.Query("category_id")
|
||||
if categoryID != "" {
|
||||
if cid, err := strconv.Atoi(categoryID); err == nil {
|
||||
query = query.Where("category_id = ?", cid)
|
||||
}
|
||||
}
|
||||
|
||||
query.Order("created_at desc").Find(&views)
|
||||
|
||||
data := fiber.Map{
|
||||
"Views": views,
|
||||
"CategoryID": categoryID,
|
||||
}
|
||||
|
||||
if c.Get("HX-Request") == "true" {
|
||||
return c.Render("admin/partials/product_category_views", data)
|
||||
}
|
||||
|
||||
return c.Render("admin/partials/product_category_views", data, "admin/layout")
|
||||
}
|
||||
237
controllers/api_cart_controller.go
Normal file
237
controllers/api_cart_controller.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
database "ares/database/config"
|
||||
"ares/database/models"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// getOrCreateCart is a helper to fetch the cart of the current user.
|
||||
func getOrCreateCart(userID uint) (models.Cart, error) {
|
||||
var cart models.Cart
|
||||
if err := database.DB.Preload("Items").Preload("Items.Product").Where("user_id = ?", userID).First(&cart).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
cart = models.Cart{UserID: userID}
|
||||
if createErr := database.DB.Create(&cart).Error; createErr != nil {
|
||||
return cart, createErr
|
||||
}
|
||||
return cart, nil
|
||||
}
|
||||
return cart, err
|
||||
}
|
||||
return cart, nil
|
||||
}
|
||||
|
||||
// GetMyCart godoc
|
||||
// @Summary Get the current user's cart
|
||||
// @Tags Cart
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} models.CartDoc
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /api/v1/cart [get]
|
||||
func GetMyCart(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(uint)
|
||||
if !ok || userID == 0 {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
cart, err := getOrCreateCart(userID)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not load cart"})
|
||||
}
|
||||
|
||||
return c.JSON(cart)
|
||||
}
|
||||
|
||||
// AddToCartRequest represents the body for adding to a cart
|
||||
type AddToCartRequest struct {
|
||||
ProductID uint `json:"product_id" validate:"required"`
|
||||
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
// AddToCart godoc
|
||||
// @Summary Add item to cart
|
||||
// @Tags Cart
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param data body AddToCartRequest true "Cart Item Details"
|
||||
// @Success 200 {object} models.CartDoc
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/cart/items [post]
|
||||
func AddToCart(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(uint)
|
||||
if !ok || userID == 0 {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
var input AddToCartRequest
|
||||
if err := c.Bind().Body(&input); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
|
||||
}
|
||||
|
||||
if err := validate.Struct(input); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Check product exists
|
||||
var product models.Product
|
||||
if err := database.DB.First(&product, input.ProductID).Error; err != nil {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "product not found"})
|
||||
}
|
||||
|
||||
cart, err := getOrCreateCart(userID)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not load cart"})
|
||||
}
|
||||
|
||||
// Check if item already exists in cart
|
||||
var existingItem models.CartItem
|
||||
if err := database.DB.Where("cart_id = ? AND product_id = ?", cart.ID, input.ProductID).First(&existingItem).Error; err == nil {
|
||||
// Update quantity
|
||||
existingItem.Quantity += input.Quantity
|
||||
database.DB.Save(&existingItem)
|
||||
} else {
|
||||
// Create new item
|
||||
newItem := models.CartItem{
|
||||
CartID: cart.ID,
|
||||
ProductID: input.ProductID,
|
||||
Quantity: input.Quantity,
|
||||
}
|
||||
database.DB.Create(&newItem)
|
||||
}
|
||||
|
||||
// Return updated cart
|
||||
cart, _ = getOrCreateCart(userID)
|
||||
return c.JSON(cart)
|
||||
}
|
||||
|
||||
// UpdateCartItemRequest represents the body for updating a cart item quantity
|
||||
type UpdateCartItemRequest struct {
|
||||
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
// UpdateCartItem godoc
|
||||
// @Summary Update cart item quantity
|
||||
// @Tags Cart
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param item_id path int true "Cart Item ID"
|
||||
// @Param data body UpdateCartItemRequest true "Update Quantity"
|
||||
// @Success 200 {object} models.CartDoc
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/cart/items/{item_id} [put]
|
||||
func UpdateCartItem(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(uint)
|
||||
if !ok || userID == 0 {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
itemID, err := strconv.ParseUint(c.Params("item_id"), 10, 32)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid item id"})
|
||||
}
|
||||
|
||||
var input UpdateCartItemRequest
|
||||
if err := c.Bind().Body(&input); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
|
||||
}
|
||||
|
||||
if err := validate.Struct(input); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
cart, err := getOrCreateCart(userID)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not load cart"})
|
||||
}
|
||||
|
||||
var cartItem models.CartItem
|
||||
if err := database.DB.Where("id = ? AND cart_id = ?", itemID, cart.ID).First(&cartItem).Error; err != nil {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "item not found in your cart"})
|
||||
}
|
||||
|
||||
cartItem.Quantity = input.Quantity
|
||||
database.DB.Save(&cartItem)
|
||||
|
||||
// Return updated cart
|
||||
cart, _ = getOrCreateCart(userID)
|
||||
return c.JSON(cart)
|
||||
}
|
||||
|
||||
// RemoveFromCart godoc
|
||||
// @Summary Remove item from cart
|
||||
// @Tags Cart
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param item_id path int true "Cart Item ID"
|
||||
// @Success 200 {object} models.CartDoc
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/cart/items/{item_id} [delete]
|
||||
func RemoveFromCart(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(uint)
|
||||
if !ok || userID == 0 {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
itemID, err := strconv.ParseUint(c.Params("item_id"), 10, 32)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid item id"})
|
||||
}
|
||||
|
||||
cart, err := getOrCreateCart(userID)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not load cart"})
|
||||
}
|
||||
|
||||
var cartItem models.CartItem
|
||||
if err := database.DB.Where("id = ? AND cart_id = ?", itemID, cart.ID).First(&cartItem).Error; err != nil {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "item not found in your cart"})
|
||||
}
|
||||
|
||||
database.DB.Delete(&cartItem)
|
||||
|
||||
// Return updated cart
|
||||
cart, _ = getOrCreateCart(userID)
|
||||
return c.JSON(cart)
|
||||
}
|
||||
|
||||
// ClearCart godoc
|
||||
// @Summary Clear the entire cart
|
||||
// @Tags Cart
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} models.CartDoc
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /api/v1/cart [delete]
|
||||
func ClearCart(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(uint)
|
||||
if !ok || userID == 0 {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
cart, err := getOrCreateCart(userID)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not load cart"})
|
||||
}
|
||||
|
||||
// Delete all items for this cart
|
||||
database.DB.Where("cart_id = ?", cart.ID).Delete(&models.CartItem{})
|
||||
|
||||
// Return updated empty cart
|
||||
cart, _ = getOrCreateCart(userID)
|
||||
return c.JSON(cart)
|
||||
}
|
||||
1123
controllers/blog_controller.go
Normal file
1123
controllers/blog_controller.go
Normal file
File diff suppressed because it is too large
Load Diff
164
controllers/hero_controller.go
Normal file
164
controllers/hero_controller.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
configs "ares/config"
|
||||
database "ares/database/config"
|
||||
"ares/database/models"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// GetHero godoc
|
||||
// @Summary Get active hero/banner
|
||||
// @Tags Hero
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/hero [get]
|
||||
func GetHero(c fiber.Ctx) error {
|
||||
var heroes []models.Hero
|
||||
// Aktif olan tüm hero'ları getir
|
||||
if err := database.DB.Where("is_active = ?", true).Find(&heroes).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
if len(heroes) == 0 {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "no active hero found"})
|
||||
}
|
||||
return c.JSON(heroes)
|
||||
}
|
||||
|
||||
// GetHeroAll godoc
|
||||
// @Summary Get all heroes
|
||||
// @Description Returns all hero/banner records (no filter)
|
||||
// @Tags Hero
|
||||
// @Produce json
|
||||
// @Success 200 {array} map[string]interface{}
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/heroes [get]
|
||||
func GetHeroAll(c fiber.Ctx) error {
|
||||
var heroes []models.Hero
|
||||
// Tüm hero'ları getir (filtre yok)
|
||||
if err := database.DB.Find(&heroes).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
if len(heroes) == 0 {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "no hero found"})
|
||||
}
|
||||
return c.JSON(heroes)
|
||||
}
|
||||
|
||||
func CreateHero(c fiber.Ctx) error {
|
||||
var hero models.Hero
|
||||
if err := c.Bind().Body(&hero); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
|
||||
// Image upload
|
||||
file, err := c.FormFile("image")
|
||||
if err == nil {
|
||||
if _, err := os.Stat("./uploads/heroes"); os.IsNotExist(err) {
|
||||
os.MkdirAll("./uploads/heroes", 0755)
|
||||
}
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
|
||||
filePath := filepath.Join("./uploads/heroes", filename)
|
||||
if err := c.SaveFile(file, filePath); err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save image"})
|
||||
}
|
||||
hero.Image = "/uploads/heroes/" + filename
|
||||
}
|
||||
|
||||
// Eğer sadece bir aktif hero olacaksa, diğerlerini pasife çekebiliriz
|
||||
//if hero.IsActive {
|
||||
// database.DB.Model(&models.Hero{}).Where("is_active = ?", true).Update("is_active", false)
|
||||
//}
|
||||
|
||||
if err := database.DB.Create(&hero).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "hero could not be created"})
|
||||
}
|
||||
|
||||
return c.Status(http.StatusCreated).JSON(hero)
|
||||
}
|
||||
|
||||
func UpdateHero(c fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
var hero models.Hero
|
||||
if err := database.DB.First(&hero, id).Error; err != nil {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "hero not found"})
|
||||
}
|
||||
|
||||
// Log raw request body (works for JSON). For multipart/form-data, also log form values.
|
||||
//log.Printf("Raw request body: %s\n", string(c.Body()))
|
||||
//log.Printf("Form title: %s, is_active: %s\n", c.FormValue("title"), c.FormValue("is_active"))
|
||||
|
||||
var updateData models.Hero
|
||||
if err := c.Bind().Body(&updateData); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
//log.Printf("Received update data: %+v\n", updateData) // Debug log
|
||||
// Image upload
|
||||
file, err := c.FormFile("image")
|
||||
if err == nil {
|
||||
if _, err := os.Stat("./uploads/heroes"); os.IsNotExist(err) {
|
||||
os.MkdirAll("./uploads/heroes", 0755)
|
||||
}
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
|
||||
filePath := filepath.Join("./uploads/heroes", filename)
|
||||
if err := c.SaveFile(file, filePath); err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save image"})
|
||||
}
|
||||
updateData.Image = "/uploads/heroes/" + filename
|
||||
}
|
||||
|
||||
// Eğer bu hero aktif yapılıyorsa diğerlerini pasife çek
|
||||
//if updateData.IsActive {
|
||||
// database.DB.Model(&models.Hero{}).Where("id != ?", id).Where("is_active = ?", true).Update("is_active", false)
|
||||
//}
|
||||
|
||||
// Handle is_active coming from multipart/form-data: parse and update explicitly
|
||||
if v := c.FormValue("is_active"); v != "" {
|
||||
if parsed, err := strconv.ParseBool(v); err == nil {
|
||||
// Ensure boolean field is updated even if it's false (zero value)
|
||||
if err := database.DB.Model(&hero).Update("is_active", parsed).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "hero could not be updated"})
|
||||
}
|
||||
// reflect into updateData for consistency
|
||||
updateData.IsActive = parsed
|
||||
} else {
|
||||
configs.Logger.Sugar().Warnf("invalid is_active value: %s", v)
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&hero).Updates(updateData).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "hero could not be updated"})
|
||||
}
|
||||
|
||||
return c.JSON(hero)
|
||||
}
|
||||
|
||||
func DeleteHero(c fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
var hero models.Hero
|
||||
if err := database.DB.First(&hero, id).Error; err != nil {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "hero not found"})
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&hero).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "hero could not be deleted"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "hero deleted successfully"})
|
||||
}
|
||||
161
controllers/product_controller.go
Normal file
161
controllers/product_controller.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
database "ares/database/config"
|
||||
"ares/database/models"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetProducts godoc
|
||||
// @Summary List products (public) with pagination
|
||||
// @Tags Products
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number"
|
||||
// @Param per_page query int false "Items per page"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/v1/products [get]
|
||||
func GetProducts(c fiber.Ctx) error {
|
||||
pageStr := c.Query("page", "1")
|
||||
perPageStr := c.Query("per_page", "10")
|
||||
page, _ := strconv.Atoi(pageStr)
|
||||
perPage, _ := strconv.Atoi(perPageStr)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if perPage < 1 {
|
||||
perPage = 10
|
||||
}
|
||||
if perPage > 100 {
|
||||
perPage = 100
|
||||
}
|
||||
offset := (page - 1) * perPage
|
||||
|
||||
var total int64
|
||||
database.DB.Model(&models.Product{}).Count(&total)
|
||||
|
||||
var products []models.Product
|
||||
database.DB.Preload("Categories").Preload("Tags").Limit(perPage).Offset(offset).Order("created_at desc").Find(&products)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"data": products,
|
||||
"meta": fiber.Map{"page": page, "per_page": perPage, "total": total},
|
||||
})
|
||||
}
|
||||
|
||||
// GetProduct godoc
|
||||
// @Summary Get single product (public) by slug
|
||||
// @Tags Products
|
||||
// @Produce json
|
||||
// @Param slug path string true "Product slug"
|
||||
// @Success 200 {object} models.ProductDoc
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/products/{slug} [get]
|
||||
func GetProduct(c fiber.Ctx) error {
|
||||
slug := c.Params("slug")
|
||||
if slug == "" {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid slug"})
|
||||
}
|
||||
|
||||
var product models.Product
|
||||
if err := database.DB.Preload("Categories").Preload("Tags").Where("slug = ? AND deleted_at IS NULL", slug).First(&product).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "product not found"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
|
||||
}
|
||||
|
||||
return c.JSON(product)
|
||||
}
|
||||
|
||||
// AddProductCommentRequest represents payload
|
||||
type AddProductCommentRequest struct {
|
||||
ProductID uint `json:"product_id" validate:"required"`
|
||||
Body string `json:"body" validate:"required,min=3"`
|
||||
}
|
||||
|
||||
|
||||
func AddProductComment(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(uint)
|
||||
if !ok || userID == 0 {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
var input AddProductCommentRequest
|
||||
if err := c.Bind().Body(&input); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
|
||||
}
|
||||
|
||||
// Add validation if needed
|
||||
if input.Body == "" {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "body is required"})
|
||||
}
|
||||
|
||||
var product models.Product
|
||||
if err := database.DB.First(&product, input.ProductID).Error; err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "product not found"})
|
||||
}
|
||||
|
||||
comment := models.ProductComment{
|
||||
UserID: userID,
|
||||
ProductID: input.ProductID,
|
||||
Body: input.Body,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&comment).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not save comment"})
|
||||
}
|
||||
|
||||
return c.Status(http.StatusCreated).JSON(comment)
|
||||
}
|
||||
|
||||
// GetProductComments godoc
|
||||
// @Summary Get comments for a product
|
||||
// @Tags Products
|
||||
// @Produce json
|
||||
// @Param id path int true "Product ID"
|
||||
// @Success 200 {array} models.ProductCommentDoc
|
||||
// @Router /api/v1/products/{id}/comments [get]
|
||||
func GetProductComments(c fiber.Ctx) error {
|
||||
productID, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid product id"})
|
||||
}
|
||||
|
||||
var comments []models.ProductComment
|
||||
database.DB.Where("product_id = ?", productID).Order("created_at desc").Find(&comments)
|
||||
|
||||
return c.JSON(comments)
|
||||
}
|
||||
|
||||
// RecordProductCategoryView godoc
|
||||
// @Summary Record a view for a product category
|
||||
// @Tags Products
|
||||
// @Produce json
|
||||
// @Param id path int true "Category ID"
|
||||
// @Success 201 {object} models.ProductCategoryViewDoc
|
||||
// @Router /api/v1/product-categories/{id}/view [post]
|
||||
func RecordProductCategoryView(c fiber.Ctx) error {
|
||||
categoryID, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid category id"})
|
||||
}
|
||||
|
||||
var category models.ProductCategory
|
||||
if err := database.DB.First(&category, categoryID).Error; err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "category not found"})
|
||||
}
|
||||
|
||||
view := models.ProductCategoryView{
|
||||
CategoryID: uint(categoryID),
|
||||
IPAddress: c.IP(),
|
||||
}
|
||||
|
||||
database.DB.Create(&view)
|
||||
|
||||
return c.Status(http.StatusCreated).JSON(view)
|
||||
}
|
||||
419
controllers/security_controller.go
Normal file
419
controllers/security_controller.go
Normal file
@@ -0,0 +1,419 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
configs "ares/config"
|
||||
database "ares/database/config"
|
||||
"ares/database/models"
|
||||
"ares/middlewares"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
corsWhitelistCacheKey = "admin:cors:whitelist:list"
|
||||
corsBlacklistCacheKey = "admin:cors:blacklist:list"
|
||||
rateLimitCacheKey = "admin:rate_limit:list"
|
||||
securityCacheTTL = 60
|
||||
)
|
||||
|
||||
type CorsWhitelistRequest struct {
|
||||
Origin string `json:"origin" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type CorsBlacklistRequest struct {
|
||||
Origin string `json:"origin" validate:"required"`
|
||||
Reason string `json:"reason"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type RateLimitSettingRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
MaxRequests int64 `json:"max_requests" validate:"required,min=1"`
|
||||
WindowSeconds int `json:"window_seconds" validate:"required,min=1"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func ListCorsWhitelists(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
var items []models.CorsWhitelist
|
||||
if cached, err := database.Get(corsWhitelistCacheKey); err == nil {
|
||||
if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil {
|
||||
securityLogf("[security][cors-whitelist][cache-hit] count=%d", len(items))
|
||||
return c.JSON(fiber.Map{"count": len(items), "items": items})
|
||||
}
|
||||
} else if !errors.Is(err, redis.Nil) {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"})
|
||||
}
|
||||
|
||||
if err := database.DB.Order("id DESC").Find(&items).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
cacheJSON, _ := json.Marshal(items)
|
||||
_ = database.SetEx(corsWhitelistCacheKey, string(cacheJSON), securityCacheTTL)
|
||||
securityLogf("[security][cors-whitelist][db-load] count=%d", len(items))
|
||||
|
||||
return c.JSON(fiber.Map{"count": len(items), "items": items})
|
||||
}
|
||||
|
||||
func CreateCorsWhitelist(c fiber.Ctx) error {
|
||||
var req CorsWhitelistRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
item := models.CorsWhitelist{
|
||||
Origin: strings.TrimSpace(req.Origin),
|
||||
Description: strings.TrimSpace(req.Description),
|
||||
IsActive: boolValue(req.IsActive, true),
|
||||
CreatedBy: currentActor(c),
|
||||
}
|
||||
if err := database.DB.Create(&item).Error; err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][cors-whitelist][create] origin=%s by=%s", item.Origin, item.CreatedBy)
|
||||
return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item})
|
||||
}
|
||||
|
||||
func UpdateCorsWhitelist(c fiber.Ctx) error {
|
||||
id, err := parseID(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
var req CorsWhitelistRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
var item models.CorsWhitelist
|
||||
if err := database.DB.First(&item, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
item.Origin = strings.TrimSpace(req.Origin)
|
||||
item.Description = strings.TrimSpace(req.Description)
|
||||
item.IsActive = boolValue(req.IsActive, item.IsActive)
|
||||
item.CreatedBy = currentActor(c)
|
||||
if err := database.DB.Save(&item).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][cors-whitelist][update] id=%d origin=%s by=%s", item.ID, item.Origin, item.CreatedBy)
|
||||
return c.JSON(fiber.Map{"item": item})
|
||||
}
|
||||
|
||||
func DeleteCorsWhitelist(c fiber.Ctx) error {
|
||||
id, err := parseID(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&models.CorsWhitelist{}, id).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][cors-whitelist][soft-delete] id=%d by=%s", id, currentActor(c))
|
||||
return c.JSON(fiber.Map{"message": "soft deleted", "id": id})
|
||||
}
|
||||
|
||||
func HardDeleteCorsWhitelist(c fiber.Ctx) error {
|
||||
id, err := parseID(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
if err := database.DB.Unscoped().Delete(&models.CorsWhitelist{}, id).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][cors-whitelist][hard-delete] id=%d by=%s", id, currentActor(c))
|
||||
return c.JSON(fiber.Map{"message": "hard deleted", "id": id})
|
||||
}
|
||||
|
||||
func ListCorsBlacklists(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
var items []models.CorsBlacklist
|
||||
if cached, err := database.Get(corsBlacklistCacheKey); err == nil {
|
||||
if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil {
|
||||
securityLogf("[security][cors-blacklist][cache-hit] count=%d", len(items))
|
||||
return c.JSON(fiber.Map{"count": len(items), "items": items})
|
||||
}
|
||||
} else if !errors.Is(err, redis.Nil) {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"})
|
||||
}
|
||||
|
||||
if err := database.DB.Order("id DESC").Find(&items).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
cacheJSON, _ := json.Marshal(items)
|
||||
_ = database.SetEx(corsBlacklistCacheKey, string(cacheJSON), securityCacheTTL)
|
||||
securityLogf("[security][cors-blacklist][db-load] count=%d", len(items))
|
||||
|
||||
return c.JSON(fiber.Map{"count": len(items), "items": items})
|
||||
}
|
||||
|
||||
func CreateCorsBlacklist(c fiber.Ctx) error {
|
||||
var req CorsBlacklistRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
item := models.CorsBlacklist{
|
||||
Origin: strings.TrimSpace(req.Origin),
|
||||
Reason: strings.TrimSpace(req.Reason),
|
||||
IsActive: boolValue(req.IsActive, true),
|
||||
CreatedBy: currentActor(c),
|
||||
}
|
||||
if err := database.DB.Create(&item).Error; err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][cors-blacklist][create] origin=%s by=%s", item.Origin, item.CreatedBy)
|
||||
return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item})
|
||||
}
|
||||
|
||||
func UpdateCorsBlacklist(c fiber.Ctx) error {
|
||||
id, err := parseID(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
var req CorsBlacklistRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
var item models.CorsBlacklist
|
||||
if err := database.DB.First(&item, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
item.Origin = strings.TrimSpace(req.Origin)
|
||||
item.Reason = strings.TrimSpace(req.Reason)
|
||||
item.IsActive = boolValue(req.IsActive, item.IsActive)
|
||||
item.CreatedBy = currentActor(c)
|
||||
if err := database.DB.Save(&item).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][cors-blacklist][update] id=%d origin=%s by=%s", item.ID, item.Origin, item.CreatedBy)
|
||||
return c.JSON(fiber.Map{"item": item})
|
||||
}
|
||||
|
||||
func DeleteCorsBlacklist(c fiber.Ctx) error {
|
||||
id, err := parseID(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&models.CorsBlacklist{}, id).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][cors-blacklist][soft-delete] id=%d by=%s", id, currentActor(c))
|
||||
return c.JSON(fiber.Map{"message": "soft deleted", "id": id})
|
||||
}
|
||||
|
||||
func HardDeleteCorsBlacklist(c fiber.Ctx) error {
|
||||
id, err := parseID(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
if err := database.DB.Unscoped().Delete(&models.CorsBlacklist{}, id).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][cors-blacklist][hard-delete] id=%d by=%s", id, currentActor(c))
|
||||
return c.JSON(fiber.Map{"message": "hard deleted", "id": id})
|
||||
}
|
||||
|
||||
func ListRateLimitSettings(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
var items []models.RateLimitSetting
|
||||
if cached, err := database.Get(rateLimitCacheKey); err == nil {
|
||||
if unmarshalErr := json.Unmarshal([]byte(cached), &items); unmarshalErr == nil {
|
||||
securityLogf("[security][rate-limit][cache-hit] count=%d", len(items))
|
||||
return c.JSON(fiber.Map{"count": len(items), "items": items})
|
||||
}
|
||||
} else if !errors.Is(err, redis.Nil) {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "cache read error"})
|
||||
}
|
||||
|
||||
if err := database.DB.Order("id DESC").Find(&items).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
cacheJSON, _ := json.Marshal(items)
|
||||
_ = database.SetEx(rateLimitCacheKey, string(cacheJSON), securityCacheTTL)
|
||||
securityLogf("[security][rate-limit][db-load] count=%d", len(items))
|
||||
|
||||
return c.JSON(fiber.Map{"count": len(items), "items": items})
|
||||
}
|
||||
|
||||
func CreateRateLimitSetting(c fiber.Ctx) error {
|
||||
var req RateLimitSettingRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
item := models.RateLimitSetting{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Description: strings.TrimSpace(req.Description),
|
||||
MaxRequests: req.MaxRequests,
|
||||
WindowSeconds: req.WindowSeconds,
|
||||
IsActive: boolValue(req.IsActive, true),
|
||||
UpdatedBy: currentActor(c),
|
||||
}
|
||||
if err := database.DB.Create(&item).Error; err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "record could not be created"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][rate-limit][create] name=%s max=%d window=%ds by=%s", item.Name, item.MaxRequests, item.WindowSeconds, item.UpdatedBy)
|
||||
return c.Status(http.StatusCreated).JSON(fiber.Map{"item": item})
|
||||
}
|
||||
|
||||
func UpdateRateLimitSetting(c fiber.Ctx) error {
|
||||
id, err := parseID(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
var req RateLimitSettingRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
var item models.RateLimitSetting
|
||||
if err := database.DB.First(&item, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
item.Name = strings.TrimSpace(req.Name)
|
||||
item.Description = strings.TrimSpace(req.Description)
|
||||
item.MaxRequests = req.MaxRequests
|
||||
item.WindowSeconds = req.WindowSeconds
|
||||
item.IsActive = boolValue(req.IsActive, item.IsActive)
|
||||
item.UpdatedBy = currentActor(c)
|
||||
if err := database.DB.Save(&item).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be updated"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][rate-limit][update] id=%d name=%s max=%d window=%ds by=%s", item.ID, item.Name, item.MaxRequests, item.WindowSeconds, item.UpdatedBy)
|
||||
return c.JSON(fiber.Map{"item": item})
|
||||
}
|
||||
|
||||
func DeleteRateLimitSetting(c fiber.Ctx) error {
|
||||
id, err := parseID(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&models.RateLimitSetting{}, id).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be deleted"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][rate-limit][soft-delete] id=%d by=%s", id, currentActor(c))
|
||||
return c.JSON(fiber.Map{"message": "soft deleted", "id": id})
|
||||
}
|
||||
|
||||
func HardDeleteRateLimitSetting(c fiber.Ctx) error {
|
||||
id, err := parseID(c.Params("id"))
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
if err := database.DB.Unscoped().Delete(&models.RateLimitSetting{}, id).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "record could not be hard-deleted"})
|
||||
}
|
||||
invalidateSecurityCaches()
|
||||
securityLogf("[security][rate-limit][hard-delete] id=%d by=%s", id, currentActor(c))
|
||||
return c.JSON(fiber.Map{"message": "hard deleted", "id": id})
|
||||
}
|
||||
|
||||
func parseID(param string) (uint, error) {
|
||||
v, err := strconv.ParseUint(strings.TrimSpace(param), 10, 64)
|
||||
if err != nil || v == 0 {
|
||||
return 0, errors.New("invalid id")
|
||||
}
|
||||
return uint(v), nil
|
||||
}
|
||||
|
||||
func invalidateSecurityCaches() {
|
||||
_ = database.Delete(corsWhitelistCacheKey)
|
||||
_ = database.Delete(corsBlacklistCacheKey)
|
||||
_ = database.Delete(rateLimitCacheKey)
|
||||
_ = database.Delete("cors:active:whitelist")
|
||||
_ = database.Delete("cors:active:blacklist")
|
||||
}
|
||||
|
||||
func currentActor(c fiber.Ctx) string {
|
||||
if claims, ok := middlewares.GetAuthClaims(c); ok && strings.TrimSpace(claims.Email) != "" {
|
||||
return claims.Email
|
||||
}
|
||||
return "system"
|
||||
}
|
||||
|
||||
func boolValue(v *bool, fallback bool) bool {
|
||||
if v == nil {
|
||||
return fallback
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func securityLogf(format string, args ...interface{}) {
|
||||
if configs.AppConfig != nil && configs.AppConfig.CorsDebug {
|
||||
if configs.Logger != nil {
|
||||
configs.Logger.Sugar().Infof(format, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
150
controllers/setting_controller.go
Normal file
150
controllers/setting_controller.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
database "ares/database/config"
|
||||
"ares/database/models"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GetSetting godoc
|
||||
// @Summary Get site settings
|
||||
// @Tags Setting
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/setting [get]
|
||||
func GetSetting(c fiber.Ctx) error {
|
||||
var setting models.Setting
|
||||
// Arkaplanda tek bir aktif ayar varsayıyoruz veya en son ekleneni/güncelleneni
|
||||
if err := database.DB.Where("is_active = ?", true).Last(&setting).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "no active setting found"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
return c.JSON(setting)
|
||||
}
|
||||
|
||||
func CreateSetting(c fiber.Ctx) error {
|
||||
var setting models.Setting
|
||||
if err := c.Bind().Body(&setting); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
|
||||
// White Logo upload
|
||||
if file, err := c.FormFile("w_logo"); err == nil {
|
||||
if _, err := os.Stat("./uploads/settings"); os.IsNotExist(err) {
|
||||
os.MkdirAll("./uploads/settings", 0755)
|
||||
}
|
||||
filename := fmt.Sprintf("w_%d_%s", time.Now().Unix(), file.Filename)
|
||||
filePath := filepath.Join("./uploads/settings", filename)
|
||||
if err := c.SaveFile(file, filePath); err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save w_logo"})
|
||||
}
|
||||
setting.WLogo = "/uploads/settings/" + filename
|
||||
}
|
||||
|
||||
// Black Logo upload
|
||||
if file, err := c.FormFile("b_logo"); err == nil {
|
||||
if _, err := os.Stat("./uploads/settings"); os.IsNotExist(err) {
|
||||
os.MkdirAll("./uploads/settings", 0755)
|
||||
}
|
||||
filename := fmt.Sprintf("b_%d_%s", time.Now().Unix(), file.Filename)
|
||||
filePath := filepath.Join("./uploads/settings", filename)
|
||||
if err := c.SaveFile(file, filePath); err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save b_logo"})
|
||||
}
|
||||
setting.BLogo = "/uploads/settings/" + filename
|
||||
}
|
||||
|
||||
// Eğer sadece bir aktif ayar olacaksa, diğerlerini pasife çekebiliriz
|
||||
if setting.IsActive {
|
||||
database.DB.Model(&models.Setting{}).Where("is_active = ?", true).Update("is_active", false)
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&setting).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "setting could not be created"})
|
||||
}
|
||||
|
||||
return c.Status(http.StatusCreated).JSON(setting)
|
||||
}
|
||||
|
||||
func UpdateSetting(c fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
var setting models.Setting
|
||||
if err := database.DB.First(&setting, id).Error; err != nil {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "setting not found"})
|
||||
}
|
||||
|
||||
var updateData models.Setting
|
||||
if err := c.Bind().Body(&updateData); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
|
||||
// White Logo upload
|
||||
if file, err := c.FormFile("w_logo"); err == nil {
|
||||
if _, err := os.Stat("./uploads/settings"); os.IsNotExist(err) {
|
||||
os.MkdirAll("./uploads/settings", 0755)
|
||||
}
|
||||
filename := fmt.Sprintf("w_%d_%s", time.Now().Unix(), file.Filename)
|
||||
filePath := filepath.Join("./uploads/settings", filename)
|
||||
if err := c.SaveFile(file, filePath); err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save w_logo"})
|
||||
}
|
||||
updateData.WLogo = "/uploads/settings/" + filename
|
||||
}
|
||||
|
||||
// Black Logo upload
|
||||
if file, err := c.FormFile("b_logo"); err == nil {
|
||||
if _, err := os.Stat("./uploads/settings"); os.IsNotExist(err) {
|
||||
os.MkdirAll("./uploads/settings", 0755)
|
||||
}
|
||||
filename := fmt.Sprintf("b_%d_%s", time.Now().Unix(), file.Filename)
|
||||
filePath := filepath.Join("./uploads/settings", filename)
|
||||
if err := c.SaveFile(file, filePath); err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "failed to save b_logo"})
|
||||
}
|
||||
updateData.BLogo = "/uploads/settings/" + filename
|
||||
}
|
||||
|
||||
// Eğer bu ayar aktif yapılıyorsa diğerlerini pasife çek
|
||||
if updateData.IsActive {
|
||||
database.DB.Model(&models.Setting{}).Where("id != ?", id).Where("is_active = ?", true).Update("is_active", false)
|
||||
}
|
||||
|
||||
if err := database.DB.Model(&setting).Updates(updateData).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "setting could not be updated"})
|
||||
}
|
||||
|
||||
return c.JSON(setting)
|
||||
}
|
||||
|
||||
func DeleteSetting(c fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid id"})
|
||||
}
|
||||
|
||||
var setting models.Setting
|
||||
if err := database.DB.First(&setting, id).Error; err != nil {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "setting not found"})
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&setting).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "setting could not be deleted"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "setting deleted successfully"})
|
||||
}
|
||||
798
controllers/user.go
Normal file
798
controllers/user.go
Normal file
@@ -0,0 +1,798 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
configs "ares/config"
|
||||
database "ares/database/config"
|
||||
"ares/database/models"
|
||||
"ares/middlewares"
|
||||
utils "ares/pkg/utis"
|
||||
"ares/services"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var validate = validator.New()
|
||||
|
||||
type RegisterRequest struct {
|
||||
UserName string `json:"username" validate:"required,min=3"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type RefreshRequest struct {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
|
||||
type ResendVerificationRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest represents allowed fields for updating a user
|
||||
type UpdateUserRequest struct {
|
||||
UserName string `json:"username,omitempty" example:"jdoe"`
|
||||
Email string `json:"email,omitempty" example:"jdoe@example.com"`
|
||||
IsAdmin *bool `json:"is_admin,omitempty" example:"false"`
|
||||
Password string `json:"password,omitempty" example:"#secret"`
|
||||
FirstName string `json:"first_name,omitempty" example:"John"`
|
||||
LastName string `json:"last_name,omitempty" example:"Doe"`
|
||||
AvatarURL string `json:"avatar_url,omitempty" example:"/uploads/avatar.jpg"`
|
||||
EmailVerified *bool `json:"email_verified,omitempty" example:"true"`
|
||||
// Accept avatar file via multipart/form-data with field name "avatar" when using form upload
|
||||
}
|
||||
|
||||
func GetUser(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusOK).SendString("Get User")
|
||||
}
|
||||
|
||||
func AdminListUsers(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
var users []models.User
|
||||
if err := database.DB.Preload("Profile").Order("id DESC").Find(&users).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": len(users),
|
||||
"users": users,
|
||||
})
|
||||
}
|
||||
|
||||
func AdminListDeletedUsers(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
var users []models.User
|
||||
if err := database.DB.Unscoped().
|
||||
Preload("Profile").
|
||||
Where("deleted_at IS NOT NULL").
|
||||
Order("deleted_at DESC").
|
||||
Find(&users).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": len(users),
|
||||
"users": users,
|
||||
})
|
||||
}
|
||||
|
||||
func GetUserOne(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusOK).SendString("Get User One")
|
||||
}
|
||||
|
||||
func UpdateUser(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.Preload("Profile").First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
// Parse incoming JSON or multipart/form-data into map to allow partial updates including false values
|
||||
var payload map[string]interface{}
|
||||
|
||||
// Prefer detecting multipart by trying to read the multipart form first
|
||||
if mf, err := c.MultipartForm(); err == nil && mf != nil {
|
||||
payload = map[string]interface{}{}
|
||||
// form values
|
||||
for k, vals := range mf.Value {
|
||||
if len(vals) > 0 {
|
||||
payload[k] = vals[0]
|
||||
}
|
||||
}
|
||||
|
||||
// handle avatar file if present
|
||||
if files, ok := mf.File["avatar"]; ok && len(files) > 0 {
|
||||
file := files[0]
|
||||
if _, err := os.Stat("./uploads/avatars"); os.IsNotExist(err) {
|
||||
os.MkdirAll("./uploads/avatars", 0755)
|
||||
}
|
||||
filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
|
||||
filePath := filepath.Join("./uploads/avatars", filename)
|
||||
if err := c.SaveFile(file, filePath); err != nil {
|
||||
if configs.Logger != nil {
|
||||
configs.Logger.Sugar().Errorf("failed to save avatar: %v", err)
|
||||
}
|
||||
} else {
|
||||
payload["avatar_url"] = "/uploads/avatars/" + filename
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// fallback to JSON body
|
||||
if err := json.Unmarshal(c.Body(), &payload); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare updates for user table
|
||||
userUpdates := map[string]interface{}{}
|
||||
if v, ok := payload["username"].(string); ok {
|
||||
userUpdates["user_name"] = v
|
||||
userUpdates["user_name"] = v
|
||||
user.UserName = v
|
||||
}
|
||||
if v, ok := payload["email"].(string); ok {
|
||||
userUpdates["email"] = v
|
||||
user.Email = v
|
||||
}
|
||||
if v, ok := payload["is_admin"]; ok {
|
||||
// handle bool or string representations
|
||||
switch val := v.(type) {
|
||||
case bool:
|
||||
userUpdates["is_admin"] = val
|
||||
user.IsAdmin = &val
|
||||
case string:
|
||||
if parsed, err := strconv.ParseBool(val); err == nil {
|
||||
userUpdates["is_admin"] = parsed
|
||||
user.IsAdmin = &parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle email_verified explicitly (bool or string)
|
||||
if v, ok := payload["email_verified"]; ok {
|
||||
switch val := v.(type) {
|
||||
case bool:
|
||||
userUpdates["email_verified"] = val
|
||||
now := time.Now()
|
||||
if val {
|
||||
userUpdates["email_verified_at"] = now
|
||||
user.EmailVerified = &val
|
||||
user.EmailVerifiedAt = &now
|
||||
} else {
|
||||
userUpdates["email_verified_at"] = nil
|
||||
user.EmailVerified = &val
|
||||
user.EmailVerifiedAt = nil
|
||||
}
|
||||
case string:
|
||||
if parsed, err := strconv.ParseBool(val); err == nil {
|
||||
userUpdates["email_verified"] = parsed
|
||||
now := time.Now()
|
||||
if parsed {
|
||||
userUpdates["email_verified_at"] = now
|
||||
user.EmailVerified = &parsed
|
||||
user.EmailVerifiedAt = &now
|
||||
} else {
|
||||
userUpdates["email_verified_at"] = nil
|
||||
user.EmailVerified = &parsed
|
||||
user.EmailVerifiedAt = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if v, ok := payload["password"].(string); ok && v != "" {
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(v), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "could not hash password"})
|
||||
}
|
||||
userUpdates["password"] = string(hashed)
|
||||
user.Password = string(hashed)
|
||||
}
|
||||
|
||||
// Apply user updates if any
|
||||
if len(userUpdates) > 0 {
|
||||
if err := database.DB.Model(&user).Updates(userUpdates).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be updated"})
|
||||
}
|
||||
}
|
||||
|
||||
// Handle profile updates (first_name, last_name, avatar_url)
|
||||
profileUpdates := map[string]interface{}{}
|
||||
if v, ok := payload["first_name"].(string); ok {
|
||||
profileUpdates["first_name"] = v
|
||||
}
|
||||
if v, ok := payload["last_name"].(string); ok {
|
||||
profileUpdates["last_name"] = v
|
||||
}
|
||||
if v, ok := payload["avatar_url"].(string); ok {
|
||||
profileUpdates["avatar_url"] = v
|
||||
}
|
||||
|
||||
if len(profileUpdates) > 0 {
|
||||
// Profile may be stored as slice; update first profile if exists else create
|
||||
var profile models.Profile
|
||||
if len(user.Profile) > 0 {
|
||||
profile = user.Profile[0]
|
||||
if err := database.DB.Model(&profile).Updates(profileUpdates).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be updated"})
|
||||
}
|
||||
} else {
|
||||
profile = models.Profile{
|
||||
UserID: uint64(user.ID),
|
||||
}
|
||||
if v, ok := profileUpdates["first_name"].(string); ok {
|
||||
profile.FirstName = v
|
||||
}
|
||||
if v, ok := profileUpdates["last_name"].(string); ok {
|
||||
profile.LastName = v
|
||||
}
|
||||
if v, ok := profileUpdates["avatar_url"].(string); ok {
|
||||
profile.AvatarURL = v
|
||||
}
|
||||
if err := database.DB.Create(&profile).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be created"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reload user with profile
|
||||
if err := database.DB.Preload("Profile").First(&user, id).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "user updated", "user": user})
|
||||
}
|
||||
|
||||
func DeleteUser(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
if err := database.DB.Delete(&user).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be deleted"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "user soft-deleted successfully",
|
||||
"user_id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func HardDeleteUser(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.Unscoped().First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
err = database.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Unscoped().Where("user_id = ?", id).Delete(&models.Profile{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Unscoped().Where("user_id = ?", id).Delete(&models.SocialAccount{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Unscoped().Delete(&models.User{}, id).Error
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user hard-delete failed"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "user permanently deleted",
|
||||
"user_id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func RestoreUser(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid user id"})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.Unscoped().First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
if !user.DeletedAt.Valid {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "user is not soft-deleted"})
|
||||
}
|
||||
|
||||
if err := database.DB.Unscoped().Model(&models.User{}).Where("id = ?", id).Update("deleted_at", nil).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "user could not be restored"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "user restored successfully",
|
||||
"user_id": id,
|
||||
})
|
||||
}
|
||||
|
||||
// Register godoc
|
||||
// @Summary Register user
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RegisterRequest true "Register payload"
|
||||
// @Success 201 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 409 {object} map[string]string
|
||||
// @Router /api/v1/auth/register [post]
|
||||
func Register(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
var req RegisterRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "password could not be hashed"})
|
||||
}
|
||||
|
||||
verifyToken, err := utils.GenerateSecureToken(32)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verify token could not be generated"})
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
UserName: req.UserName,
|
||||
Email: req.Email,
|
||||
Password: string(hashedPassword),
|
||||
EmailVerifyToken: verifyToken,
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&user).Error; err != nil {
|
||||
return c.Status(http.StatusConflict).JSON(fiber.Map{"error": "email already exists"})
|
||||
}
|
||||
|
||||
profile := models.Profile{
|
||||
UserID: uint64(user.ID),
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
}
|
||||
if err := database.DB.Create(&profile).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "profile could not be created"})
|
||||
}
|
||||
|
||||
appURL := strings.TrimRight(configs.AppConfig.AppURL, "/")
|
||||
verifyURL := fmt.Sprintf("%s/api/v1/auth/verify-email?token=%s", appURL, url.QueryEscape(verifyToken))
|
||||
|
||||
emailService := services.NewEmailService()
|
||||
err = emailService.SendVerificationEmail(user.Email, profile.FirstName, verifyURL)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification email could not be sent"})
|
||||
}
|
||||
|
||||
return c.Status(http.StatusCreated).JSON(fiber.Map{
|
||||
"message": "registration successful, please verify your email before login",
|
||||
"user": fiber.Map{
|
||||
"id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"is_admin": boolPtrValue(user.IsAdmin),
|
||||
"email_verified": false,
|
||||
"first_name": profile.FirstName,
|
||||
"last_name": profile.LastName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Login godoc
|
||||
// @Summary Login user
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body LoginRequest true "Login payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /api/v1/auth/login [post]
|
||||
func Login(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
var req LoginRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.Preload("Profile").Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid email or password"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid email or password"})
|
||||
}
|
||||
|
||||
if !user.IsEmailVerified() {
|
||||
return c.Status(http.StatusForbidden).JSON(fiber.Map{"error": "please verify your email before login"})
|
||||
}
|
||||
|
||||
firstName, lastName := extractProfileName(user.Profile)
|
||||
jwtService := services.NewJWTService()
|
||||
accessToken, refreshToken, err := jwtService.GenerateTokenPair(
|
||||
user.ID,
|
||||
user.Email,
|
||||
boolPtrValue(user.IsAdmin),
|
||||
firstName,
|
||||
lastName,
|
||||
)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "tokens could not be generated"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"user": fiber.Map{
|
||||
"id": user.ID,
|
||||
"username": user.UserName,
|
||||
"email": user.Email,
|
||||
"is_admin": boolPtrValue(user.IsAdmin),
|
||||
"first_name": firstName,
|
||||
"last_name": lastName,
|
||||
},
|
||||
"access_token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
// @Summary Refresh access token
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body RefreshRequest true "Refresh payload"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /api/v1/auth/refresh [post]
|
||||
func RefreshToken(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
var req RefreshRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
jwtService := services.NewJWTService()
|
||||
claims, err := jwtService.ValidateToken(req.RefreshToken)
|
||||
if err != nil || claims.TokenType != services.TokenTypeRefresh {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "invalid refresh token"})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.Preload("Profile").First(&user, claims.UserID).Error; err != nil {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "user not found"})
|
||||
}
|
||||
|
||||
firstName, lastName := extractProfileName(user.Profile)
|
||||
accessToken, refreshToken, err := jwtService.GenerateTokenPair(
|
||||
user.ID,
|
||||
user.Email,
|
||||
boolPtrValue(user.IsAdmin),
|
||||
firstName,
|
||||
lastName,
|
||||
)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "tokens could not be generated"})
|
||||
}
|
||||
fmt.Println(accessToken, "Access Token Yenilendi !!!")
|
||||
return c.JSON(fiber.Map{
|
||||
"access_token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyEmail godoc
|
||||
// @Summary Verify email address with token
|
||||
// @Tags Auth
|
||||
// @Produce json
|
||||
// @Param token query string true "Email verify token"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/auth/verify-email [get]
|
||||
func VerifyEmail(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(c.Query("token"))
|
||||
if token == "" {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "token is required"})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.Where("email_verify_token = ?", token).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "invalid or expired token"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
isVerified := true
|
||||
user.EmailVerified = &isVerified
|
||||
user.EmailVerifiedAt = &now
|
||||
user.EmailVerifyToken = ""
|
||||
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "email verification could not be saved"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "email verified successfully"})
|
||||
}
|
||||
|
||||
// ResendVerificationEmail godoc
|
||||
// @Summary Resend verification email
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body ResendVerificationRequest true "Resend verification payload"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/auth/resend-verification [post]
|
||||
func ResendVerificationEmail(c fiber.Ctx) error {
|
||||
if database.DB == nil {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "database is not configured"})
|
||||
}
|
||||
|
||||
var req ResendVerificationRequest
|
||||
if err := c.Bind().JSON(&req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
if err := validate.Struct(req); err != nil {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := database.DB.Preload("Profile").Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
|
||||
}
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
|
||||
}
|
||||
|
||||
if user.IsEmailVerified() {
|
||||
return c.JSON(fiber.Map{"message": "email is already verified"})
|
||||
}
|
||||
|
||||
verifyToken, err := utils.GenerateSecureToken(32)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verify token could not be generated"})
|
||||
}
|
||||
user.EmailVerifyToken = verifyToken
|
||||
if err := database.DB.Save(&user).Error; err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification token could not be saved"})
|
||||
}
|
||||
|
||||
firstName, _ := extractProfileName(user.Profile)
|
||||
appURL := strings.TrimRight(configs.AppConfig.AppURL, "/")
|
||||
verifyURL := fmt.Sprintf("%s/api/v1/auth/verify-email?token=%s", appURL, url.QueryEscape(verifyToken))
|
||||
|
||||
emailService := services.NewEmailService()
|
||||
if err := emailService.SendVerificationEmail(user.Email, firstName, verifyURL); err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "verification email could not be sent"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "verification email has been sent"})
|
||||
}
|
||||
|
||||
// Me godoc
|
||||
// @Summary Get current user from token
|
||||
// @Tags Auth
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /api/v1/auth/me [get]
|
||||
func Me(c fiber.Ctx) error {
|
||||
claims, ok := middlewares.GetAuthClaims(c)
|
||||
if !ok {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"user": fiber.Map{
|
||||
"id": claims.UserID,
|
||||
"email": claims.Email,
|
||||
"is_admin": claims.IsAdmin,
|
||||
"first_name": claims.FirstName,
|
||||
"last_name": claims.LastName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func AdminOnlyExample(c fiber.Ctx) error {
|
||||
claims, ok := middlewares.GetAuthClaims(c)
|
||||
if !ok {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "only admins can access this endpoint",
|
||||
"user": claims.Email,
|
||||
})
|
||||
}
|
||||
|
||||
func UserOnlyExample(c fiber.Ctx) error {
|
||||
claims, ok := middlewares.GetAuthClaims(c)
|
||||
if !ok {
|
||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized"})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "only normal users can access this endpoint",
|
||||
"user": claims.Email,
|
||||
})
|
||||
}
|
||||
|
||||
func GoogleAuth(c fiber.Ctx) error {
|
||||
if configs.AppConfig.GoogleClientID == "" {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "google oauth is not configured"})
|
||||
}
|
||||
|
||||
stateToken, err := utils.GenerateSecureToken(16)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "state token could not be generated"})
|
||||
}
|
||||
|
||||
authURL := "https://accounts.google.com/o/oauth2/v2/auth?" + url.Values{
|
||||
"client_id": []string{configs.AppConfig.GoogleClientID},
|
||||
"redirect_uri": []string{configs.AppConfig.GoogleRedirectURL},
|
||||
"response_type": []string{"code"},
|
||||
"scope": []string{"openid email profile"},
|
||||
"state": []string{stateToken},
|
||||
}.Encode()
|
||||
|
||||
return c.JSON(fiber.Map{"provider": "google", "auth_url": authURL, "state": stateToken})
|
||||
}
|
||||
|
||||
func GoogleAuthCallback(c fiber.Ctx) error {
|
||||
code := c.Query("code")
|
||||
if code == "" {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "google callback code is missing"})
|
||||
}
|
||||
|
||||
// OAuth token exchange is intentionally left simple for now.
|
||||
return c.JSON(fiber.Map{
|
||||
"provider": "google",
|
||||
"message": "google callback infrastructure is ready, token exchange can be added next",
|
||||
"code": code,
|
||||
"state": c.Query("state"),
|
||||
})
|
||||
}
|
||||
|
||||
func GithubAuth(c fiber.Ctx) error {
|
||||
if configs.AppConfig.GithubClientID == "" {
|
||||
return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{"error": "github oauth is not configured"})
|
||||
}
|
||||
|
||||
stateToken, err := utils.GenerateSecureToken(16)
|
||||
if err != nil {
|
||||
return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": "state token could not be generated"})
|
||||
}
|
||||
|
||||
authURL := "https://github.com/login/oauth/authorize?" + url.Values{
|
||||
"client_id": []string{configs.AppConfig.GithubClientID},
|
||||
"redirect_uri": []string{configs.AppConfig.GithubRedirectURL},
|
||||
"scope": []string{"read:user user:email"},
|
||||
"state": []string{stateToken},
|
||||
}.Encode()
|
||||
|
||||
return c.JSON(fiber.Map{"provider": "github", "auth_url": authURL, "state": stateToken})
|
||||
}
|
||||
|
||||
func GithubAuthCallback(c fiber.Ctx) error {
|
||||
code := c.Query("code")
|
||||
if code == "" {
|
||||
return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "github callback code is missing"})
|
||||
}
|
||||
|
||||
// OAuth token exchange is intentionally left simple for now.
|
||||
return c.JSON(fiber.Map{
|
||||
"provider": "github",
|
||||
"message": "github callback infrastructure is ready, token exchange can be added next",
|
||||
"code": code,
|
||||
"state": c.Query("state"),
|
||||
})
|
||||
}
|
||||
|
||||
func extractProfileName(profiles []models.Profile) (string, string) {
|
||||
if len(profiles) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
return profiles[0].FirstName, profiles[0].LastName
|
||||
}
|
||||
|
||||
func boolPtrValue(v *bool) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
return *v
|
||||
}
|
||||
Reference in New Issue
Block a user