Files
Trackeep/backend/handlers/admin.go
T

389 lines
11 KiB
Go

package handlers
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/trackeep/backend/config"
"github.com/trackeep/backend/models"
"golang.org/x/crypto/bcrypt"
)
// AdminMiddleware checks if user is admin
func AdminMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
userID := c.GetUint("userID")
if userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
c.Abort()
return
}
var user models.User
db := config.GetDB()
if err := db.First(&user, userID).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
c.Abort()
return
}
if user.Role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
c.Abort()
return
}
c.Set("user", user)
c.Next()
}
}
// AdminGetAllLearningPaths handles GET /api/v1/admin/learning-paths
func AdminGetAllLearningPaths(c *gin.Context) {
db := config.GetDB()
var learningPaths []models.LearningPath
// Parse query parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
status := c.Query("status")
creator := c.Query("creator")
offset := (page - 1) * limit
query := db.Model(&models.LearningPath{})
// Add filters
if status == "published" {
query = query.Where("is_published = ?", true)
} else if status == "draft" {
query = query.Where("is_published = ?", false)
}
if creator != "" {
// Escape special SQL characters to prevent SQL injection
escapedCreator := strings.ReplaceAll(creator, "%", "\\%")
escapedCreator = strings.ReplaceAll(escapedCreator, "_", "\\_")
query = query.Joins("JOIN users ON users.id = learning_paths.creator_id").
Where("users.username ILIKE ? OR users.full_name ILIKE ?", "%"+escapedCreator+"%", "%"+escapedCreator+"%")
}
// Count total records
var total int64
query.Count(&total)
// Fetch learning paths with relationships
if err := query.Preload("Creator").
Preload("Tags").
Offset(offset).
Limit(limit).
Order("created_at DESC").
Find(&learningPaths).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch learning paths"})
return
}
c.JSON(http.StatusOK, gin.H{
"learning_paths": learningPaths,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total,
"pages": (total + int64(limit) - 1) / int64(limit),
},
})
}
// AdminReviewLearningPath handles PUT /api/v1/admin/learning-paths/:id/review
func AdminReviewLearningPath(c *gin.Context) {
db := config.GetDB()
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid learning path ID"})
return
}
var input struct {
Action string `json:"action" binding:"required"` // approve, reject, feature
IsPublished *bool `json:"is_published"`
IsFeatured *bool `json:"is_featured"`
AdminNotes string `json:"admin_notes"`
RejectReason string `json:"reject_reason"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var learningPath models.LearningPath
if err := db.Preload("Creator").First(&learningPath, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Learning path not found"})
return
}
// Perform action based on input
switch input.Action {
case "approve":
if input.IsPublished != nil {
learningPath.IsPublished = *input.IsPublished
} else {
learningPath.IsPublished = true
}
case "reject":
learningPath.IsPublished = false
// Could add rejection reason field to model if needed
case "feature":
if input.IsFeatured != nil {
learningPath.IsFeatured = *input.IsFeatured
} else {
learningPath.IsFeatured = true
}
case "unfeature":
learningPath.IsFeatured = false
}
if err := db.Save(&learningPath).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update learning path"})
return
}
// Log admin action (could implement audit log here)
c.JSON(http.StatusOK, gin.H{
"message": "Learning path reviewed successfully",
"learning_path": learningPath,
})
}
// AdminGetUsers handles GET /api/v1/admin/users
func AdminGetUsers(c *gin.Context) {
db := config.GetDB()
var users []models.User
// Parse query parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
role := c.Query("role")
search := c.Query("search")
offset := (page - 1) * limit
query := db.Model(&models.User{})
// Add filters
if role != "" {
query = query.Where("role = ?", role)
}
if search != "" {
// Escape special SQL characters to prevent SQL injection
escapedSearch := strings.ReplaceAll(search, "%", "\\%")
escapedSearch = strings.ReplaceAll(escapedSearch, "_", "\\_")
query = query.Where("username ILIKE ? OR full_name ILIKE ? OR email ILIKE ?",
"%"+escapedSearch+"%", "%"+escapedSearch+"%", "%"+escapedSearch+"%")
}
// Count total records
var total int64
query.Count(&total)
// Fetch users
if err := query.Offset(offset).
Limit(limit).
Order("created_at DESC").
Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"})
return
}
// Remove passwords from response
for i := range users {
users[i].Password = ""
}
c.JSON(http.StatusOK, gin.H{
"users": users,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total,
"pages": (total + int64(limit) - 1) / int64(limit),
},
})
}
// AdminCreateUser handles POST /api/v1/admin/users
func AdminCreateUser(c *gin.Context) {
db := config.GetDB()
var req struct {
Email string `json:"email" binding:"required,email"`
Username string `json:"username" binding:"required,min=3,max=50"`
Password string `json:"password" binding:"required,min=6"`
FullName string `json:"fullName" binding:"required,min=1,max=100"`
Role string `json:"role"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
role := req.Role
if role == "" {
role = "user"
}
if role != "user" && role != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role. Must be 'user' or 'admin'"})
return
}
var existing models.User
if err := db.Where("email = ?", req.Email).First(&existing).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "User with this email already exists"})
return
}
if err := db.Where("username = ?", req.Username).First(&existing).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Username already taken"})
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
user := models.User{
Email: req.Email,
Username: req.Username,
Password: string(hashedPassword),
FullName: req.FullName,
Role: role,
Theme: "dark",
}
if err := db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}
_ = ensureMessagingDefaults(db, user.ID)
user.Password = ""
c.JSON(http.StatusCreated, gin.H{
"message": "User created successfully",
"user": user,
})
}
// AdminUpdateUserRole handles PUT /api/v1/admin/users/:id/role
func AdminUpdateUserRole(c *gin.Context) {
db := config.GetDB()
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
var input struct {
Role string `json:"role" binding:"required"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Validate role
if input.Role != "user" && input.Role != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid role. Must be 'user' or 'admin'"})
return
}
var user models.User
if err := db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
// Prevent admin from changing their own role
currentUserID := c.GetUint("userID")
if currentUserID == uint(id) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change your own role"})
return
}
user.Role = input.Role
if err := db.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user role"})
return
}
// Remove password from response
user.Password = ""
c.JSON(http.StatusOK, gin.H{
"message": "User role updated successfully",
"user": user,
})
}
// AdminGetStats handles GET /api/v1/admin/stats
func AdminGetStats(c *gin.Context) {
db := config.GetDB()
var stats struct {
TotalUsers int64 `json:"total_users"`
AdminUsers int64 `json:"admin_users"`
TotalLearningPaths int64 `json:"total_learning_paths"`
PublishedPaths int64 `json:"published_paths"`
DraftPaths int64 `json:"draft_paths"`
FeaturedPaths int64 `json:"featured_paths"`
TotalEnrollments int64 `json:"total_enrollments"`
ActiveEnrollments int64 `json:"active_enrollments"`
CompletedEnrollments int64 `json:"completed_enrollments"`
}
// User stats
db.Model(&models.User{}).Count(&stats.TotalUsers)
db.Model(&models.User{}).Where("role = ?", "admin").Count(&stats.AdminUsers)
// Learning path stats
db.Model(&models.LearningPath{}).Count(&stats.TotalLearningPaths)
db.Model(&models.LearningPath{}).Where("is_published = ?", true).Count(&stats.PublishedPaths)
db.Model(&models.LearningPath{}).Where("is_published = ?", false).Count(&stats.DraftPaths)
db.Model(&models.LearningPath{}).Where("is_featured = ?", true).Count(&stats.FeaturedPaths)
// Enrollment stats
db.Model(&models.Enrollment{}).Count(&stats.TotalEnrollments)
db.Model(&models.Enrollment{}).Where("status = ?", "in_progress").Count(&stats.ActiveEnrollments)
db.Model(&models.Enrollment{}).Where("status = ?", "completed").Count(&stats.CompletedEnrollments)
c.JSON(http.StatusOK, stats)
}
// AdminDeleteLearningPath handles DELETE /api/v1/admin/learning-paths/:id
func AdminDeleteLearningPath(c *gin.Context) {
db := config.GetDB()
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid learning path ID"})
return
}
var learningPath models.LearningPath
if err := db.First(&learningPath, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Learning path not found"})
return
}
if err := db.Delete(&learningPath).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete learning path"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Learning path deleted successfully"})
}