Add admin panel pages for additional models
- Add confirmation codes management page - Add devices management page - Add feature benefits management page - Add password reset codes management page - Add promotions management page - Add share codes management page - Add corresponding backend handlers and routes - Update sidebar navigation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
164
internal/admin/handlers/confirmation_code_handler.go
Normal file
164
internal/admin/handlers/confirmation_code_handler.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/admin/dto"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminConfirmationCodeHandler handles admin confirmation code management endpoints
|
||||
type AdminConfirmationCodeHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminConfirmationCodeHandler creates a new admin confirmation code handler
|
||||
func NewAdminConfirmationCodeHandler(db *gorm.DB) *AdminConfirmationCodeHandler {
|
||||
return &AdminConfirmationCodeHandler{db: db}
|
||||
}
|
||||
|
||||
// ConfirmationCodeResponse represents a confirmation code in API responses
|
||||
type ConfirmationCodeResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
IsUsed bool `json:"is_used"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/confirmation-codes
|
||||
func (h *AdminConfirmationCodeHandler) List(c *gin.Context) {
|
||||
var filters dto.PaginationParams
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var codes []models.ConfirmationCode
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.ConfirmationCode{}).Preload("User")
|
||||
|
||||
// Apply search (search by user info or code)
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Joins("JOIN auth_user ON auth_user.id = user_confirmationcode.user_id").
|
||||
Where(
|
||||
"auth_user.username ILIKE ? OR auth_user.email ILIKE ? OR user_confirmationcode.code ILIKE ?",
|
||||
search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&codes).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch confirmation codes"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]ConfirmationCodeResponse, len(codes))
|
||||
for i, code := range codes {
|
||||
responses[i] = ConfirmationCodeResponse{
|
||||
ID: code.ID,
|
||||
UserID: code.UserID,
|
||||
Username: code.User.Username,
|
||||
Email: code.User.Email,
|
||||
Code: code.Code,
|
||||
ExpiresAt: code.ExpiresAt.Format("2006-01-02T15:04:05Z"),
|
||||
IsUsed: code.IsUsed,
|
||||
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
|
||||
}
|
||||
|
||||
// Get handles GET /api/admin/confirmation-codes/:id
|
||||
func (h *AdminConfirmationCodeHandler) Get(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var code models.ConfirmationCode
|
||||
if err := h.db.Preload("User").First(&code, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Confirmation code not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch confirmation code"})
|
||||
return
|
||||
}
|
||||
|
||||
response := ConfirmationCodeResponse{
|
||||
ID: code.ID,
|
||||
UserID: code.UserID,
|
||||
Username: code.User.Username,
|
||||
Email: code.User.Email,
|
||||
Code: code.Code,
|
||||
ExpiresAt: code.ExpiresAt.Format("2006-01-02T15:04:05Z"),
|
||||
IsUsed: code.IsUsed,
|
||||
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/confirmation-codes/:id
|
||||
func (h *AdminConfirmationCodeHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Delete(&models.ConfirmationCode{}, id)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete confirmation code"})
|
||||
return
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Confirmation code not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Confirmation code deleted successfully"})
|
||||
}
|
||||
|
||||
// BulkDelete handles DELETE /api/admin/confirmation-codes/bulk
|
||||
func (h *AdminConfirmationCodeHandler) BulkDelete(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Where("id IN ?", req.IDs).Delete(&models.ConfirmationCode{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete confirmation codes"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Confirmation codes deleted successfully", "count": result.RowsAffected})
|
||||
}
|
||||
340
internal/admin/handlers/device_handler.go
Normal file
340
internal/admin/handlers/device_handler.go
Normal file
@@ -0,0 +1,340 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/admin/dto"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminDeviceHandler handles admin device management endpoints
|
||||
type AdminDeviceHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminDeviceHandler creates a new admin device handler
|
||||
func NewAdminDeviceHandler(db *gorm.DB) *AdminDeviceHandler {
|
||||
return &AdminDeviceHandler{db: db}
|
||||
}
|
||||
|
||||
// APNSDeviceResponse represents an iOS device in API responses
|
||||
type APNSDeviceResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
UserID *uint `json:"user_id"`
|
||||
Username *string `json:"username"`
|
||||
DeviceID string `json:"device_id"`
|
||||
RegistrationID string `json:"registration_id"`
|
||||
DateCreated string `json:"date_created"`
|
||||
}
|
||||
|
||||
// GCMDeviceResponse represents an Android device in API responses
|
||||
type GCMDeviceResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
UserID *uint `json:"user_id"`
|
||||
Username *string `json:"username"`
|
||||
DeviceID string `json:"device_id"`
|
||||
RegistrationID string `json:"registration_id"`
|
||||
CloudMessageType string `json:"cloud_message_type"`
|
||||
DateCreated string `json:"date_created"`
|
||||
}
|
||||
|
||||
// ListAPNS handles GET /api/admin/devices/apns
|
||||
func (h *AdminDeviceHandler) ListAPNS(c *gin.Context) {
|
||||
var filters dto.PaginationParams
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var devices []models.APNSDevice
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.APNSDevice{}).Preload("User")
|
||||
|
||||
// Apply search
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Joins("LEFT JOIN auth_user ON auth_user.id = push_notifications_apnsdevice.user_id").
|
||||
Where(
|
||||
"push_notifications_apnsdevice.name ILIKE ? OR push_notifications_apnsdevice.device_id ILIKE ? OR auth_user.username ILIKE ?",
|
||||
search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
sortBy := "date_created"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&devices).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch devices"})
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]APNSDeviceResponse, len(devices))
|
||||
for i, device := range devices {
|
||||
var username *string
|
||||
if device.User != nil {
|
||||
username = &device.User.Username
|
||||
}
|
||||
responses[i] = APNSDeviceResponse{
|
||||
ID: device.ID,
|
||||
Name: device.Name,
|
||||
Active: device.Active,
|
||||
UserID: device.UserID,
|
||||
Username: username,
|
||||
DeviceID: device.DeviceID,
|
||||
RegistrationID: device.RegistrationID[:min(20, len(device.RegistrationID))] + "...",
|
||||
DateCreated: device.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
|
||||
}
|
||||
|
||||
// ListGCM handles GET /api/admin/devices/gcm
|
||||
func (h *AdminDeviceHandler) ListGCM(c *gin.Context) {
|
||||
var filters dto.PaginationParams
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var devices []models.GCMDevice
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.GCMDevice{}).Preload("User")
|
||||
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Joins("LEFT JOIN auth_user ON auth_user.id = push_notifications_gcmdevice.user_id").
|
||||
Where(
|
||||
"push_notifications_gcmdevice.name ILIKE ? OR push_notifications_gcmdevice.device_id ILIKE ? OR auth_user.username ILIKE ?",
|
||||
search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
sortBy := "date_created"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&devices).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch devices"})
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]GCMDeviceResponse, len(devices))
|
||||
for i, device := range devices {
|
||||
var username *string
|
||||
if device.User != nil {
|
||||
username = &device.User.Username
|
||||
}
|
||||
responses[i] = GCMDeviceResponse{
|
||||
ID: device.ID,
|
||||
Name: device.Name,
|
||||
Active: device.Active,
|
||||
UserID: device.UserID,
|
||||
Username: username,
|
||||
DeviceID: device.DeviceID,
|
||||
RegistrationID: device.RegistrationID[:min(20, len(device.RegistrationID))] + "...",
|
||||
CloudMessageType: device.CloudMessageType,
|
||||
DateCreated: device.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
|
||||
}
|
||||
|
||||
// UpdateAPNS handles PUT /api/admin/devices/apns/:id
|
||||
func (h *AdminDeviceHandler) UpdateAPNS(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var device models.APNSDevice
|
||||
if err := h.db.First(&device, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch device"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
device.Active = req.Active
|
||||
if err := h.db.Save(&device).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update device"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Device updated successfully"})
|
||||
}
|
||||
|
||||
// UpdateGCM handles PUT /api/admin/devices/gcm/:id
|
||||
func (h *AdminDeviceHandler) UpdateGCM(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var device models.GCMDevice
|
||||
if err := h.db.First(&device, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch device"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
device.Active = req.Active
|
||||
if err := h.db.Save(&device).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update device"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Device updated successfully"})
|
||||
}
|
||||
|
||||
// DeleteAPNS handles DELETE /api/admin/devices/apns/:id
|
||||
func (h *AdminDeviceHandler) DeleteAPNS(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Delete(&models.APNSDevice{}, id)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete device"})
|
||||
return
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Device deleted successfully"})
|
||||
}
|
||||
|
||||
// DeleteGCM handles DELETE /api/admin/devices/gcm/:id
|
||||
func (h *AdminDeviceHandler) DeleteGCM(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Delete(&models.GCMDevice{}, id)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete device"})
|
||||
return
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Device deleted successfully"})
|
||||
}
|
||||
|
||||
// BulkDeleteAPNS handles DELETE /api/admin/devices/apns/bulk
|
||||
func (h *AdminDeviceHandler) BulkDeleteAPNS(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Where("id IN ?", req.IDs).Delete(&models.APNSDevice{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete devices"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Devices deleted successfully", "count": result.RowsAffected})
|
||||
}
|
||||
|
||||
// BulkDeleteGCM handles DELETE /api/admin/devices/gcm/bulk
|
||||
func (h *AdminDeviceHandler) BulkDeleteGCM(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Where("id IN ?", req.IDs).Delete(&models.GCMDevice{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete devices"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Devices deleted successfully", "count": result.RowsAffected})
|
||||
}
|
||||
|
||||
// GetStats handles GET /api/admin/devices/stats
|
||||
func (h *AdminDeviceHandler) GetStats(c *gin.Context) {
|
||||
var apnsTotal, apnsActive, gcmTotal, gcmActive int64
|
||||
|
||||
h.db.Model(&models.APNSDevice{}).Count(&apnsTotal)
|
||||
h.db.Model(&models.APNSDevice{}).Where("active = ?", true).Count(&apnsActive)
|
||||
h.db.Model(&models.GCMDevice{}).Count(&gcmTotal)
|
||||
h.db.Model(&models.GCMDevice{}).Where("active = ?", true).Count(&gcmActive)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"apns": gin.H{
|
||||
"total": apnsTotal,
|
||||
"active": apnsActive,
|
||||
},
|
||||
"gcm": gin.H{
|
||||
"total": gcmTotal,
|
||||
"active": gcmActive,
|
||||
},
|
||||
"total": apnsTotal + gcmTotal,
|
||||
})
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
245
internal/admin/handlers/feature_benefit_handler.go
Normal file
245
internal/admin/handlers/feature_benefit_handler.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/admin/dto"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminFeatureBenefitHandler handles admin feature benefit management endpoints
|
||||
type AdminFeatureBenefitHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminFeatureBenefitHandler creates a new admin feature benefit handler
|
||||
func NewAdminFeatureBenefitHandler(db *gorm.DB) *AdminFeatureBenefitHandler {
|
||||
return &AdminFeatureBenefitHandler{db: db}
|
||||
}
|
||||
|
||||
// FeatureBenefitResponse represents a feature benefit in API responses
|
||||
type FeatureBenefitResponse struct {
|
||||
ID uint `json:"id"`
|
||||
FeatureName string `json:"feature_name"`
|
||||
FreeTierText string `json:"free_tier_text"`
|
||||
ProTierText string `json:"pro_tier_text"`
|
||||
DisplayOrder int `json:"display_order"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/feature-benefits
|
||||
func (h *AdminFeatureBenefitHandler) List(c *gin.Context) {
|
||||
var filters dto.PaginationParams
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var benefits []models.FeatureBenefit
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.FeatureBenefit{})
|
||||
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Where("feature_name ILIKE ? OR free_tier_text ILIKE ? OR pro_tier_text ILIKE ?", search, search, search)
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
sortBy := "display_order"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&benefits).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch feature benefits"})
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]FeatureBenefitResponse, len(benefits))
|
||||
for i, b := range benefits {
|
||||
responses[i] = FeatureBenefitResponse{
|
||||
ID: b.ID,
|
||||
FeatureName: b.FeatureName,
|
||||
FreeTierText: b.FreeTierText,
|
||||
ProTierText: b.ProTierText,
|
||||
DisplayOrder: b.DisplayOrder,
|
||||
IsActive: b.IsActive,
|
||||
CreatedAt: b.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: b.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
|
||||
}
|
||||
|
||||
// Get handles GET /api/admin/feature-benefits/:id
|
||||
func (h *AdminFeatureBenefitHandler) Get(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var benefit models.FeatureBenefit
|
||||
if err := h.db.First(&benefit, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Feature benefit not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch feature benefit"})
|
||||
return
|
||||
}
|
||||
|
||||
response := FeatureBenefitResponse{
|
||||
ID: benefit.ID,
|
||||
FeatureName: benefit.FeatureName,
|
||||
FreeTierText: benefit.FreeTierText,
|
||||
ProTierText: benefit.ProTierText,
|
||||
DisplayOrder: benefit.DisplayOrder,
|
||||
IsActive: benefit.IsActive,
|
||||
CreatedAt: benefit.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: benefit.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Create handles POST /api/admin/feature-benefits
|
||||
func (h *AdminFeatureBenefitHandler) Create(c *gin.Context) {
|
||||
var req struct {
|
||||
FeatureName string `json:"feature_name" binding:"required"`
|
||||
FreeTierText string `json:"free_tier_text" binding:"required"`
|
||||
ProTierText string `json:"pro_tier_text" binding:"required"`
|
||||
DisplayOrder int `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
benefit := models.FeatureBenefit{
|
||||
FeatureName: req.FeatureName,
|
||||
FreeTierText: req.FreeTierText,
|
||||
ProTierText: req.ProTierText,
|
||||
DisplayOrder: req.DisplayOrder,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if req.IsActive != nil {
|
||||
benefit.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
if err := h.db.Create(&benefit).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create feature benefit"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, FeatureBenefitResponse{
|
||||
ID: benefit.ID,
|
||||
FeatureName: benefit.FeatureName,
|
||||
FreeTierText: benefit.FreeTierText,
|
||||
ProTierText: benefit.ProTierText,
|
||||
DisplayOrder: benefit.DisplayOrder,
|
||||
IsActive: benefit.IsActive,
|
||||
CreatedAt: benefit.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: benefit.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/feature-benefits/:id
|
||||
func (h *AdminFeatureBenefitHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var benefit models.FeatureBenefit
|
||||
if err := h.db.First(&benefit, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Feature benefit not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch feature benefit"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
FeatureName *string `json:"feature_name"`
|
||||
FreeTierText *string `json:"free_tier_text"`
|
||||
ProTierText *string `json:"pro_tier_text"`
|
||||
DisplayOrder *int `json:"display_order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.FeatureName != nil {
|
||||
benefit.FeatureName = *req.FeatureName
|
||||
}
|
||||
if req.FreeTierText != nil {
|
||||
benefit.FreeTierText = *req.FreeTierText
|
||||
}
|
||||
if req.ProTierText != nil {
|
||||
benefit.ProTierText = *req.ProTierText
|
||||
}
|
||||
if req.DisplayOrder != nil {
|
||||
benefit.DisplayOrder = *req.DisplayOrder
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
benefit.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
if err := h.db.Save(&benefit).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update feature benefit"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, FeatureBenefitResponse{
|
||||
ID: benefit.ID,
|
||||
FeatureName: benefit.FeatureName,
|
||||
FreeTierText: benefit.FreeTierText,
|
||||
ProTierText: benefit.ProTierText,
|
||||
DisplayOrder: benefit.DisplayOrder,
|
||||
IsActive: benefit.IsActive,
|
||||
CreatedAt: benefit.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: benefit.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/feature-benefits/:id
|
||||
func (h *AdminFeatureBenefitHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Delete(&models.FeatureBenefit{}, id)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete feature benefit"})
|
||||
return
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Feature benefit not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Feature benefit deleted successfully"})
|
||||
}
|
||||
170
internal/admin/handlers/password_reset_code_handler.go
Normal file
170
internal/admin/handlers/password_reset_code_handler.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/admin/dto"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminPasswordResetCodeHandler handles admin password reset code management endpoints
|
||||
type AdminPasswordResetCodeHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminPasswordResetCodeHandler creates a new admin password reset code handler
|
||||
func NewAdminPasswordResetCodeHandler(db *gorm.DB) *AdminPasswordResetCodeHandler {
|
||||
return &AdminPasswordResetCodeHandler{db: db}
|
||||
}
|
||||
|
||||
// PasswordResetCodeResponse represents a password reset code in API responses
|
||||
type PasswordResetCodeResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
ResetToken string `json:"reset_token"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
Used bool `json:"used"`
|
||||
Attempts int `json:"attempts"`
|
||||
MaxAttempts int `json:"max_attempts"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/password-reset-codes
|
||||
func (h *AdminPasswordResetCodeHandler) List(c *gin.Context) {
|
||||
var filters dto.PaginationParams
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var codes []models.PasswordResetCode
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.PasswordResetCode{}).Preload("User")
|
||||
|
||||
// Apply search (search by user info or token)
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Joins("JOIN auth_user ON auth_user.id = user_passwordresetcode.user_id").
|
||||
Where(
|
||||
"auth_user.username ILIKE ? OR auth_user.email ILIKE ? OR user_passwordresetcode.reset_token ILIKE ?",
|
||||
search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&codes).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch password reset codes"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]PasswordResetCodeResponse, len(codes))
|
||||
for i, code := range codes {
|
||||
responses[i] = PasswordResetCodeResponse{
|
||||
ID: code.ID,
|
||||
UserID: code.UserID,
|
||||
Username: code.User.Username,
|
||||
Email: code.User.Email,
|
||||
ResetToken: code.ResetToken[:8] + "..." + code.ResetToken[len(code.ResetToken)-4:], // Truncate for display
|
||||
ExpiresAt: code.ExpiresAt.Format("2006-01-02T15:04:05Z"),
|
||||
Used: code.Used,
|
||||
Attempts: code.Attempts,
|
||||
MaxAttempts: code.MaxAttempts,
|
||||
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
|
||||
}
|
||||
|
||||
// Get handles GET /api/admin/password-reset-codes/:id
|
||||
func (h *AdminPasswordResetCodeHandler) Get(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var code models.PasswordResetCode
|
||||
if err := h.db.Preload("User").First(&code, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Password reset code not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch password reset code"})
|
||||
return
|
||||
}
|
||||
|
||||
response := PasswordResetCodeResponse{
|
||||
ID: code.ID,
|
||||
UserID: code.UserID,
|
||||
Username: code.User.Username,
|
||||
Email: code.User.Email,
|
||||
ResetToken: code.ResetToken[:8] + "..." + code.ResetToken[len(code.ResetToken)-4:],
|
||||
ExpiresAt: code.ExpiresAt.Format("2006-01-02T15:04:05Z"),
|
||||
Used: code.Used,
|
||||
Attempts: code.Attempts,
|
||||
MaxAttempts: code.MaxAttempts,
|
||||
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/password-reset-codes/:id
|
||||
func (h *AdminPasswordResetCodeHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Delete(&models.PasswordResetCode{}, id)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete password reset code"})
|
||||
return
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Password reset code not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Password reset code deleted successfully"})
|
||||
}
|
||||
|
||||
// BulkDelete handles DELETE /api/admin/password-reset-codes/bulk
|
||||
func (h *AdminPasswordResetCodeHandler) BulkDelete(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Where("id IN ?", req.IDs).Delete(&models.PasswordResetCode{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete password reset codes"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Password reset codes deleted successfully", "count": result.RowsAffected})
|
||||
}
|
||||
322
internal/admin/handlers/promotion_handler.go
Normal file
322
internal/admin/handlers/promotion_handler.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/admin/dto"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminPromotionHandler handles admin promotion management endpoints
|
||||
type AdminPromotionHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminPromotionHandler creates a new admin promotion handler
|
||||
func NewAdminPromotionHandler(db *gorm.DB) *AdminPromotionHandler {
|
||||
return &AdminPromotionHandler{db: db}
|
||||
}
|
||||
|
||||
// PromotionResponse represents a promotion in API responses
|
||||
type PromotionResponse struct {
|
||||
ID uint `json:"id"`
|
||||
PromotionID string `json:"promotion_id"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Link *string `json:"link"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
TargetTier string `json:"target_tier"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/promotions
|
||||
func (h *AdminPromotionHandler) List(c *gin.Context) {
|
||||
var filters dto.PaginationParams
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var promotions []models.Promotion
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.Promotion{})
|
||||
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Where("promotion_id ILIKE ? OR title ILIKE ? OR message ILIKE ?", search, search, search)
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&promotions).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch promotions"})
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]PromotionResponse, len(promotions))
|
||||
for i, p := range promotions {
|
||||
responses[i] = PromotionResponse{
|
||||
ID: p.ID,
|
||||
PromotionID: p.PromotionID,
|
||||
Title: p.Title,
|
||||
Message: p.Message,
|
||||
Link: p.Link,
|
||||
StartDate: p.StartDate.Format("2006-01-02T15:04:05Z"),
|
||||
EndDate: p.EndDate.Format("2006-01-02T15:04:05Z"),
|
||||
TargetTier: string(p.TargetTier),
|
||||
IsActive: p.IsActive,
|
||||
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: p.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
|
||||
}
|
||||
|
||||
// Get handles GET /api/admin/promotions/:id
|
||||
func (h *AdminPromotionHandler) Get(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var promotion models.Promotion
|
||||
if err := h.db.First(&promotion, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Promotion not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch promotion"})
|
||||
return
|
||||
}
|
||||
|
||||
response := PromotionResponse{
|
||||
ID: promotion.ID,
|
||||
PromotionID: promotion.PromotionID,
|
||||
Title: promotion.Title,
|
||||
Message: promotion.Message,
|
||||
Link: promotion.Link,
|
||||
StartDate: promotion.StartDate.Format("2006-01-02T15:04:05Z"),
|
||||
EndDate: promotion.EndDate.Format("2006-01-02T15:04:05Z"),
|
||||
TargetTier: string(promotion.TargetTier),
|
||||
IsActive: promotion.IsActive,
|
||||
CreatedAt: promotion.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: promotion.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Create handles POST /api/admin/promotions
|
||||
func (h *AdminPromotionHandler) Create(c *gin.Context) {
|
||||
var req struct {
|
||||
PromotionID string `json:"promotion_id" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Message string `json:"message" binding:"required"`
|
||||
Link *string `json:"link"`
|
||||
StartDate string `json:"start_date" binding:"required"`
|
||||
EndDate string `json:"end_date" binding:"required"`
|
||||
TargetTier string `json:"target_tier"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
startDate, err := time.Parse("2006-01-02T15:04:05Z", req.StartDate)
|
||||
if err != nil {
|
||||
startDate, err = time.Parse("2006-01-02", req.StartDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start_date format"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
endDate, err := time.Parse("2006-01-02T15:04:05Z", req.EndDate)
|
||||
if err != nil {
|
||||
endDate, err = time.Parse("2006-01-02", req.EndDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end_date format"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
targetTier := models.TierFree
|
||||
if req.TargetTier == "pro" {
|
||||
targetTier = models.TierPro
|
||||
}
|
||||
|
||||
promotion := models.Promotion{
|
||||
PromotionID: req.PromotionID,
|
||||
Title: req.Title,
|
||||
Message: req.Message,
|
||||
Link: req.Link,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
TargetTier: targetTier,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if req.IsActive != nil {
|
||||
promotion.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
if err := h.db.Create(&promotion).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create promotion"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, PromotionResponse{
|
||||
ID: promotion.ID,
|
||||
PromotionID: promotion.PromotionID,
|
||||
Title: promotion.Title,
|
||||
Message: promotion.Message,
|
||||
Link: promotion.Link,
|
||||
StartDate: promotion.StartDate.Format("2006-01-02T15:04:05Z"),
|
||||
EndDate: promotion.EndDate.Format("2006-01-02T15:04:05Z"),
|
||||
TargetTier: string(promotion.TargetTier),
|
||||
IsActive: promotion.IsActive,
|
||||
CreatedAt: promotion.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: promotion.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/promotions/:id
|
||||
func (h *AdminPromotionHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var promotion models.Promotion
|
||||
if err := h.db.First(&promotion, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Promotion not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch promotion"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
PromotionID *string `json:"promotion_id"`
|
||||
Title *string `json:"title"`
|
||||
Message *string `json:"message"`
|
||||
Link *string `json:"link"`
|
||||
StartDate *string `json:"start_date"`
|
||||
EndDate *string `json:"end_date"`
|
||||
TargetTier *string `json:"target_tier"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.PromotionID != nil {
|
||||
promotion.PromotionID = *req.PromotionID
|
||||
}
|
||||
if req.Title != nil {
|
||||
promotion.Title = *req.Title
|
||||
}
|
||||
if req.Message != nil {
|
||||
promotion.Message = *req.Message
|
||||
}
|
||||
if req.Link != nil {
|
||||
promotion.Link = req.Link
|
||||
}
|
||||
if req.StartDate != nil {
|
||||
startDate, err := time.Parse("2006-01-02T15:04:05Z", *req.StartDate)
|
||||
if err != nil {
|
||||
startDate, err = time.Parse("2006-01-02", *req.StartDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start_date format"})
|
||||
return
|
||||
}
|
||||
}
|
||||
promotion.StartDate = startDate
|
||||
}
|
||||
if req.EndDate != nil {
|
||||
endDate, err := time.Parse("2006-01-02T15:04:05Z", *req.EndDate)
|
||||
if err != nil {
|
||||
endDate, err = time.Parse("2006-01-02", *req.EndDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end_date format"})
|
||||
return
|
||||
}
|
||||
}
|
||||
promotion.EndDate = endDate
|
||||
}
|
||||
if req.TargetTier != nil {
|
||||
if *req.TargetTier == "pro" {
|
||||
promotion.TargetTier = models.TierPro
|
||||
} else {
|
||||
promotion.TargetTier = models.TierFree
|
||||
}
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
promotion.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
if err := h.db.Save(&promotion).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update promotion"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, PromotionResponse{
|
||||
ID: promotion.ID,
|
||||
PromotionID: promotion.PromotionID,
|
||||
Title: promotion.Title,
|
||||
Message: promotion.Message,
|
||||
Link: promotion.Link,
|
||||
StartDate: promotion.StartDate.Format("2006-01-02T15:04:05Z"),
|
||||
EndDate: promotion.EndDate.Format("2006-01-02T15:04:05Z"),
|
||||
TargetTier: string(promotion.TargetTier),
|
||||
IsActive: promotion.IsActive,
|
||||
CreatedAt: promotion.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: promotion.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/promotions/:id
|
||||
func (h *AdminPromotionHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Delete(&models.Promotion{}, id)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete promotion"})
|
||||
return
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Promotion not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Promotion deleted successfully"})
|
||||
}
|
||||
239
internal/admin/handlers/share_code_handler.go
Normal file
239
internal/admin/handlers/share_code_handler.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/admin/dto"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
)
|
||||
|
||||
// AdminShareCodeHandler handles admin share code management endpoints
|
||||
type AdminShareCodeHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAdminShareCodeHandler creates a new admin share code handler
|
||||
func NewAdminShareCodeHandler(db *gorm.DB) *AdminShareCodeHandler {
|
||||
return &AdminShareCodeHandler{db: db}
|
||||
}
|
||||
|
||||
// ShareCodeResponse represents a share code in API responses
|
||||
type ShareCodeResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ResidenceID uint `json:"residence_id"`
|
||||
ResidenceName string `json:"residence_name"`
|
||||
Code string `json:"code"`
|
||||
CreatedByID uint `json:"created_by_id"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// List handles GET /api/admin/share-codes
|
||||
func (h *AdminShareCodeHandler) List(c *gin.Context) {
|
||||
var filters dto.PaginationParams
|
||||
if err := c.ShouldBindQuery(&filters); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var codes []models.ResidenceShareCode
|
||||
var total int64
|
||||
|
||||
query := h.db.Model(&models.ResidenceShareCode{}).
|
||||
Preload("Residence").
|
||||
Preload("CreatedBy")
|
||||
|
||||
// Apply search (search by code or residence name)
|
||||
if filters.Search != "" {
|
||||
search := "%" + filters.Search + "%"
|
||||
query = query.Joins("JOIN residence_residence ON residence_residence.id = residence_residencesharecode.residence_id").
|
||||
Joins("JOIN auth_user ON auth_user.id = residence_residencesharecode.created_by_id").
|
||||
Where(
|
||||
"residence_residencesharecode.code ILIKE ? OR residence_residence.name ILIKE ? OR auth_user.username ILIKE ?",
|
||||
search, search, search,
|
||||
)
|
||||
}
|
||||
|
||||
// Get total count
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortBy := "created_at"
|
||||
if filters.SortBy != "" {
|
||||
sortBy = filters.SortBy
|
||||
}
|
||||
query = query.Order(sortBy + " " + filters.GetSortDir())
|
||||
|
||||
// Apply pagination
|
||||
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
||||
|
||||
if err := query.Find(&codes).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch share codes"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build response
|
||||
responses := make([]ShareCodeResponse, len(codes))
|
||||
for i, code := range codes {
|
||||
var expiresAt *string
|
||||
if code.ExpiresAt != nil {
|
||||
formatted := code.ExpiresAt.Format("2006-01-02T15:04:05Z")
|
||||
expiresAt = &formatted
|
||||
}
|
||||
|
||||
responses[i] = ShareCodeResponse{
|
||||
ID: code.ID,
|
||||
ResidenceID: code.ResidenceID,
|
||||
ResidenceName: code.Residence.Name,
|
||||
Code: code.Code,
|
||||
CreatedByID: code.CreatedByID,
|
||||
CreatedBy: code.CreatedBy.Username,
|
||||
IsActive: code.IsActive,
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
|
||||
}
|
||||
|
||||
// Get handles GET /api/admin/share-codes/:id
|
||||
func (h *AdminShareCodeHandler) Get(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var code models.ResidenceShareCode
|
||||
if err := h.db.Preload("Residence").Preload("CreatedBy").First(&code, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Share code not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch share code"})
|
||||
return
|
||||
}
|
||||
|
||||
var expiresAt *string
|
||||
if code.ExpiresAt != nil {
|
||||
formatted := code.ExpiresAt.Format("2006-01-02T15:04:05Z")
|
||||
expiresAt = &formatted
|
||||
}
|
||||
|
||||
response := ShareCodeResponse{
|
||||
ID: code.ID,
|
||||
ResidenceID: code.ResidenceID,
|
||||
ResidenceName: code.Residence.Name,
|
||||
Code: code.Code,
|
||||
CreatedByID: code.CreatedByID,
|
||||
CreatedBy: code.CreatedBy.Username,
|
||||
IsActive: code.IsActive,
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Update handles PUT /api/admin/share-codes/:id
|
||||
func (h *AdminShareCodeHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var code models.ResidenceShareCode
|
||||
if err := h.db.First(&code, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Share code not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch share code"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
code.IsActive = req.IsActive
|
||||
|
||||
if err := h.db.Save(&code).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update share code"})
|
||||
return
|
||||
}
|
||||
|
||||
// Reload with relations
|
||||
h.db.Preload("Residence").Preload("CreatedBy").First(&code, id)
|
||||
|
||||
var expiresAt *string
|
||||
if code.ExpiresAt != nil {
|
||||
formatted := code.ExpiresAt.Format("2006-01-02T15:04:05Z")
|
||||
expiresAt = &formatted
|
||||
}
|
||||
|
||||
response := ShareCodeResponse{
|
||||
ID: code.ID,
|
||||
ResidenceID: code.ResidenceID,
|
||||
ResidenceName: code.Residence.Name,
|
||||
Code: code.Code,
|
||||
CreatedByID: code.CreatedByID,
|
||||
CreatedBy: code.CreatedBy.Username,
|
||||
IsActive: code.IsActive,
|
||||
ExpiresAt: expiresAt,
|
||||
CreatedAt: code.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/admin/share-codes/:id
|
||||
func (h *AdminShareCodeHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Delete(&models.ResidenceShareCode{}, id)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete share code"})
|
||||
return
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Share code not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Share code deleted successfully"})
|
||||
}
|
||||
|
||||
// BulkDelete handles DELETE /api/admin/share-codes/bulk
|
||||
func (h *AdminShareCodeHandler) BulkDelete(c *gin.Context) {
|
||||
var req dto.BulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
result := h.db.Where("id IN ?", req.IDs).Delete(&models.ResidenceShareCode{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete share codes"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Share codes deleted successfully", "count": result.RowsAffected})
|
||||
}
|
||||
@@ -171,6 +171,74 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Depe
|
||||
completions.DELETE("/:id", completionHandler.Delete)
|
||||
}
|
||||
|
||||
// Confirmation code management
|
||||
confirmationCodeHandler := handlers.NewAdminConfirmationCodeHandler(db)
|
||||
confirmationCodes := protected.Group("/confirmation-codes")
|
||||
{
|
||||
confirmationCodes.GET("", confirmationCodeHandler.List)
|
||||
confirmationCodes.DELETE("/bulk", confirmationCodeHandler.BulkDelete)
|
||||
confirmationCodes.GET("/:id", confirmationCodeHandler.Get)
|
||||
confirmationCodes.DELETE("/:id", confirmationCodeHandler.Delete)
|
||||
}
|
||||
|
||||
// Share code management
|
||||
shareCodeHandler := handlers.NewAdminShareCodeHandler(db)
|
||||
shareCodes := protected.Group("/share-codes")
|
||||
{
|
||||
shareCodes.GET("", shareCodeHandler.List)
|
||||
shareCodes.DELETE("/bulk", shareCodeHandler.BulkDelete)
|
||||
shareCodes.GET("/:id", shareCodeHandler.Get)
|
||||
shareCodes.PUT("/:id", shareCodeHandler.Update)
|
||||
shareCodes.DELETE("/:id", shareCodeHandler.Delete)
|
||||
}
|
||||
|
||||
// Password reset code management
|
||||
passwordResetCodeHandler := handlers.NewAdminPasswordResetCodeHandler(db)
|
||||
passwordResetCodes := protected.Group("/password-reset-codes")
|
||||
{
|
||||
passwordResetCodes.GET("", passwordResetCodeHandler.List)
|
||||
passwordResetCodes.DELETE("/bulk", passwordResetCodeHandler.BulkDelete)
|
||||
passwordResetCodes.GET("/:id", passwordResetCodeHandler.Get)
|
||||
passwordResetCodes.DELETE("/:id", passwordResetCodeHandler.Delete)
|
||||
}
|
||||
|
||||
// Push notification devices management
|
||||
deviceHandler := handlers.NewAdminDeviceHandler(db)
|
||||
devices := protected.Group("/devices")
|
||||
{
|
||||
devices.GET("/stats", deviceHandler.GetStats)
|
||||
devices.GET("/apns", deviceHandler.ListAPNS)
|
||||
devices.DELETE("/apns/bulk", deviceHandler.BulkDeleteAPNS)
|
||||
devices.PUT("/apns/:id", deviceHandler.UpdateAPNS)
|
||||
devices.DELETE("/apns/:id", deviceHandler.DeleteAPNS)
|
||||
devices.GET("/gcm", deviceHandler.ListGCM)
|
||||
devices.DELETE("/gcm/bulk", deviceHandler.BulkDeleteGCM)
|
||||
devices.PUT("/gcm/:id", deviceHandler.UpdateGCM)
|
||||
devices.DELETE("/gcm/:id", deviceHandler.DeleteGCM)
|
||||
}
|
||||
|
||||
// Feature benefits management
|
||||
featureBenefitHandler := handlers.NewAdminFeatureBenefitHandler(db)
|
||||
featureBenefits := protected.Group("/feature-benefits")
|
||||
{
|
||||
featureBenefits.GET("", featureBenefitHandler.List)
|
||||
featureBenefits.POST("", featureBenefitHandler.Create)
|
||||
featureBenefits.GET("/:id", featureBenefitHandler.Get)
|
||||
featureBenefits.PUT("/:id", featureBenefitHandler.Update)
|
||||
featureBenefits.DELETE("/:id", featureBenefitHandler.Delete)
|
||||
}
|
||||
|
||||
// Promotions management
|
||||
promotionHandler := handlers.NewAdminPromotionHandler(db)
|
||||
promotions := protected.Group("/promotions")
|
||||
{
|
||||
promotions.GET("", promotionHandler.List)
|
||||
promotions.POST("", promotionHandler.Create)
|
||||
promotions.GET("/:id", promotionHandler.Get)
|
||||
promotions.PUT("/:id", promotionHandler.Update)
|
||||
promotions.DELETE("/:id", promotionHandler.Delete)
|
||||
}
|
||||
|
||||
// Lookup tables management
|
||||
lookupHandler := handlers.NewAdminLookupHandler(db)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user