Comprehensive security hardening from audit findings: - Add validation tags to all DTO request structs (max lengths, ranges, enums) - Replace unsafe type assertions with MustGetAuthUser helper across all handlers - Remove query-param token auth from admin middleware (prevents URL token leakage) - Add request validation calls in handlers that were missing c.Validate() - Remove goroutines in handlers (timezone update now synchronous) - Add sanitize middleware and path traversal protection (path_utils) - Stop resetting admin passwords on migration restart - Warn on well-known default SECRET_KEY - Add ~30 new test files covering security regressions, auth safety, repos, and services - Add deploy/ config, audit digests, and AUDIT_FINDINGS documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
5.1 KiB
Go
154 lines
5.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"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 echo.Context) error {
|
|
var filters dto.PaginationParams
|
|
if err := c.Bind(&filters); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
|
|
}
|
|
|
|
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 (allowlist prevents SQL injection via sort_by parameter)
|
|
sortBy := filters.GetSafeSortBy([]string{
|
|
"id", "user_id", "created_at", "expires_at", "is_used",
|
|
}, "created_at")
|
|
query = query.Order(sortBy + " " + filters.GetSortDir())
|
|
|
|
// Apply pagination
|
|
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
|
|
|
|
if err := query.Find(&codes).Error; err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch confirmation codes"})
|
|
}
|
|
|
|
// 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"),
|
|
}
|
|
}
|
|
|
|
return 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 echo.Context) error {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
|
|
}
|
|
|
|
var code models.ConfirmationCode
|
|
if err := h.db.Preload("User").First(&code, id).Error; err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Confirmation code not found"})
|
|
}
|
|
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch confirmation code"})
|
|
}
|
|
|
|
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"),
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// Delete handles DELETE /api/admin/confirmation-codes/:id
|
|
func (h *AdminConfirmationCodeHandler) Delete(c echo.Context) error {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid ID"})
|
|
}
|
|
|
|
result := h.db.Delete(&models.ConfirmationCode{}, id)
|
|
if result.Error != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete confirmation code"})
|
|
}
|
|
|
|
if result.RowsAffected == 0 {
|
|
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Confirmation code not found"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Confirmation code deleted successfully"})
|
|
}
|
|
|
|
// BulkDelete handles DELETE /api/admin/confirmation-codes/bulk
|
|
func (h *AdminConfirmationCodeHandler) BulkDelete(c echo.Context) error {
|
|
var req dto.BulkDeleteRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
|
|
}
|
|
|
|
result := h.db.Where("id IN ?", req.IDs).Delete(&models.ConfirmationCode{})
|
|
if result.Error != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete confirmation codes"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Confirmation codes deleted successfully", "count": result.RowsAffected})
|
|
}
|