mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
389 lines
11 KiB
Go
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"})
|
|
}
|