mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
- Eliminated the log generator feature from the admin logs template and related handlers. - Updated icons in the log viewer for consistency and improved visual clarity. - Adjusted mobile dropdown functionality in the layout template for better user experience. - Cleaned up debug logging statements in the WebSocket handler for recent logs.
1725 lines
49 KiB
Go
1725 lines
49 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/starfleetcptn/gomft/components"
|
|
"github.com/starfleetcptn/gomft/internal/db"
|
|
)
|
|
|
|
// HandleAdminDashboard renders the admin dashboard page
|
|
func (h *Handlers) HandleAdminDashboard(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
_ = components.AdminLayout(ctx).Render(c.Request.Context(), c.Writer)
|
|
}
|
|
|
|
// HandleRoles renders the role management page
|
|
func (h *Handlers) HandleRoles(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
|
|
// Fetch roles from the database
|
|
var dbRoles []db.Role
|
|
if err := h.DB.Find(&dbRoles).Error; err != nil {
|
|
data := components.RolesData{
|
|
Error: "Failed to fetch roles: " + err.Error(),
|
|
}
|
|
_ = components.AdminRoles(ctx, data).Render(ctx, c.Writer)
|
|
return
|
|
}
|
|
|
|
// Convert db.Role to components.Role
|
|
roles := make([]components.Role, len(dbRoles))
|
|
for i, role := range dbRoles {
|
|
roles[i] = components.Role{
|
|
ID: role.ID,
|
|
Name: role.Name,
|
|
Description: role.Description,
|
|
Permissions: role.GetPermissions(),
|
|
}
|
|
}
|
|
|
|
data := components.RolesData{
|
|
Roles: roles,
|
|
}
|
|
|
|
_ = components.AdminRoles(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
|
|
// HandleNewRole renders the new role creation page
|
|
func (h *Handlers) HandleNewRole(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
|
|
data := components.RoleFormData{
|
|
Role: &components.Role{},
|
|
IsNew: true,
|
|
AllPermissions: GetAllPermissions(),
|
|
}
|
|
|
|
_ = components.AdminRoleForm(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
|
|
// HandleCreateRole processes role creation
|
|
func (h *Handlers) HandleCreateRole(c *gin.Context) {
|
|
name := c.PostForm("name")
|
|
description := c.PostForm("description")
|
|
permissions := c.PostFormArray("permissions[]")
|
|
|
|
// Create role
|
|
role := &db.Role{
|
|
Name: name,
|
|
Description: description,
|
|
}
|
|
role.SetPermissions(permissions)
|
|
|
|
// Validate role
|
|
if err := role.Validate(); err != nil {
|
|
ctx := components.CreateTemplateContext(c)
|
|
data := components.RoleFormData{
|
|
Role: &components.Role{Name: name, Description: description, Permissions: permissions},
|
|
IsNew: true,
|
|
ErrorMessage: err.Error(),
|
|
AllPermissions: GetAllPermissions(),
|
|
}
|
|
_ = components.AdminRoleForm(ctx, data).Render(ctx, c.Writer)
|
|
return
|
|
}
|
|
|
|
// Start transaction
|
|
tx := h.DB.Begin()
|
|
if err := tx.Error; err != nil {
|
|
handleRoleError(c, role, true, "Failed to begin transaction: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Create role in database
|
|
if err := tx.Create(role).Error; err != nil {
|
|
tx.Rollback()
|
|
handleRoleError(c, role, true, "Failed to create role: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Create audit log
|
|
userID := getUserID(c) // Implement this helper to get current user ID
|
|
if err := role.AuditLog(tx, "create", userID); err != nil {
|
|
tx.Rollback()
|
|
handleRoleError(c, role, true, "Failed to create audit log: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Commit transaction
|
|
if err := tx.Commit().Error; err != nil {
|
|
handleRoleError(c, role, true, "Failed to commit transaction: "+err.Error())
|
|
return
|
|
}
|
|
|
|
c.Redirect(http.StatusFound, "/admin/roles")
|
|
}
|
|
|
|
// HandleEditRole renders the role edit page
|
|
func (h *Handlers) HandleEditRole(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
id := c.Param("id")
|
|
|
|
var role db.Role
|
|
if err := h.DB.First(&role, id).Error; err != nil {
|
|
c.Redirect(http.StatusFound, "/admin/roles")
|
|
return
|
|
}
|
|
|
|
data := components.RoleFormData{
|
|
Role: &components.Role{
|
|
ID: role.ID,
|
|
Name: role.Name,
|
|
Description: role.Description,
|
|
Permissions: role.GetPermissions(),
|
|
},
|
|
IsNew: false,
|
|
AllPermissions: GetAllPermissions(),
|
|
}
|
|
|
|
_ = components.AdminRoleForm(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
|
|
// HandleUpdateRole processes role updates
|
|
func (h *Handlers) HandleUpdateRole(c *gin.Context) {
|
|
id := c.Param("id")
|
|
name := c.PostForm("name")
|
|
description := c.PostForm("description")
|
|
permissions := c.PostFormArray("permissions[]")
|
|
|
|
// Get existing role
|
|
var role db.Role
|
|
if err := h.DB.First(&role, id).Error; err != nil {
|
|
c.Redirect(http.StatusFound, "/admin/roles")
|
|
return
|
|
}
|
|
|
|
// Check if this is a system role
|
|
if role.IsSystemRole() {
|
|
handleRoleError(c, &role, false, "Cannot modify system role")
|
|
return
|
|
}
|
|
|
|
// Update role fields
|
|
role.Name = name
|
|
role.Description = description
|
|
role.SetPermissions(permissions)
|
|
|
|
// Validate role
|
|
if err := role.Validate(); err != nil {
|
|
handleRoleError(c, &role, false, err.Error())
|
|
return
|
|
}
|
|
|
|
// Start transaction
|
|
tx := h.DB.Begin()
|
|
if err := tx.Error; err != nil {
|
|
handleRoleError(c, &role, false, "Failed to begin transaction: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Update role in database
|
|
if err := tx.Save(&role).Error; err != nil {
|
|
tx.Rollback()
|
|
handleRoleError(c, &role, false, "Failed to update role: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Create audit log
|
|
userID := getUserID(c)
|
|
if err := role.AuditLog(tx, "update", userID); err != nil {
|
|
tx.Rollback()
|
|
handleRoleError(c, &role, false, "Failed to create audit log: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Commit transaction
|
|
if err := tx.Commit().Error; err != nil {
|
|
handleRoleError(c, &role, false, "Failed to commit transaction: "+err.Error())
|
|
return
|
|
}
|
|
|
|
c.Redirect(http.StatusFound, "/admin/roles")
|
|
}
|
|
|
|
// HandleDeleteRole processes role deletion
|
|
func (h *Handlers) HandleDeleteRole(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
// Get existing role
|
|
var role db.Role
|
|
if err := h.DB.First(&role, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Role not found"})
|
|
return
|
|
}
|
|
|
|
// Check if this is a system role
|
|
if role.IsSystemRole() {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete system role"})
|
|
return
|
|
}
|
|
|
|
// Start transaction
|
|
tx := h.DB.Begin()
|
|
if err := tx.Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to begin transaction"})
|
|
return
|
|
}
|
|
|
|
// Create audit log before deletion
|
|
userID := getUserID(c)
|
|
if err := role.AuditLog(tx, "delete", userID); err != nil {
|
|
tx.Rollback()
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create audit log"})
|
|
return
|
|
}
|
|
|
|
// Delete role
|
|
if err := tx.Delete(&role).Error; err != nil {
|
|
tx.Rollback()
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete role"})
|
|
return
|
|
}
|
|
|
|
// Commit transaction
|
|
if err := tx.Commit().Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction"})
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// GetAllPermissions returns a list of all available permissions
|
|
func GetAllPermissions() []string {
|
|
return []string{
|
|
"users.view",
|
|
"users.create",
|
|
"users.edit",
|
|
"users.delete",
|
|
"roles.view",
|
|
"roles.create",
|
|
"roles.edit",
|
|
"roles.delete",
|
|
"configs.view",
|
|
"configs.create",
|
|
"configs.edit",
|
|
"configs.delete",
|
|
"jobs.view",
|
|
"jobs.create",
|
|
"jobs.edit",
|
|
"jobs.delete",
|
|
"jobs.run",
|
|
"audit.view",
|
|
"audit.export",
|
|
"system.settings",
|
|
"system.backup",
|
|
"system.restore",
|
|
}
|
|
}
|
|
|
|
// Helper function to parse uint from string
|
|
func parseUint(s string) uint {
|
|
id, _ := strconv.ParseUint(s, 10, 32)
|
|
return uint(id)
|
|
}
|
|
|
|
// HandleAuditLogs renders the audit logs page
|
|
func (h *Handlers) HandleAuditLogs(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
|
|
// Get filter parameters from query
|
|
action := c.Query("action")
|
|
entity := c.Query("entity")
|
|
user := c.Query("user")
|
|
dateFrom := c.Query("date_from")
|
|
dateTo := c.Query("date_to")
|
|
|
|
// Get page parameter
|
|
page, _ := strconv.Atoi(c.Query("page"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
|
|
// Items per page
|
|
const perPage = 20
|
|
|
|
// Initialize base query
|
|
query := h.DB.Model(&db.AuditLog{}).Order("timestamp DESC")
|
|
|
|
// Apply filters
|
|
if action != "" {
|
|
query = query.Where("action = ?", action)
|
|
}
|
|
|
|
if entity != "" {
|
|
query = query.Where("entity_type = ?", entity)
|
|
}
|
|
|
|
if user != "" {
|
|
// Join with users table to search by username
|
|
query = query.Joins("LEFT JOIN users ON audit_logs.user_id = users.id").
|
|
Where("users.email LIKE ?", "%"+user+"%")
|
|
}
|
|
|
|
if dateFrom != "" {
|
|
fromDate, err := time.Parse("2006-01-02", dateFrom)
|
|
if err == nil {
|
|
query = query.Where("timestamp >= ?", fromDate)
|
|
}
|
|
}
|
|
|
|
if dateTo != "" {
|
|
toDate, err := time.Parse("2006-01-02", dateTo)
|
|
if err == nil {
|
|
// Add one day to include the end date
|
|
toDate = toDate.Add(24 * time.Hour)
|
|
query = query.Where("timestamp < ?", toDate)
|
|
}
|
|
}
|
|
|
|
// Count total records
|
|
var totalRecords int64
|
|
if err := query.Count(&totalRecords).Error; err != nil {
|
|
h.HandleError(c, http.StatusInternalServerError, "Database error", err.Error(), err)
|
|
return
|
|
}
|
|
|
|
// Calculate pagination
|
|
totalPages := int(math.Ceil(float64(totalRecords) / float64(perPage)))
|
|
offset := (page - 1) * perPage
|
|
|
|
// Get logs for current page
|
|
var dbLogs []db.AuditLog
|
|
if err := query.Limit(perPage).Offset(offset).Find(&dbLogs).Error; err != nil {
|
|
h.HandleError(c, http.StatusInternalServerError, "Database error", err.Error(), err)
|
|
return
|
|
}
|
|
|
|
// Get usernames for user IDs
|
|
userIDs := make([]uint, len(dbLogs))
|
|
for i, log := range dbLogs {
|
|
userIDs[i] = log.UserID
|
|
}
|
|
|
|
var users []db.User
|
|
if err := h.DB.Where("id IN ?", userIDs).Find(&users).Error; err != nil {
|
|
h.HandleError(c, http.StatusInternalServerError, "Database error", err.Error(), err)
|
|
return
|
|
}
|
|
|
|
// Create map of user IDs to usernames
|
|
userMap := make(map[uint]string)
|
|
for _, u := range users {
|
|
userMap[u.ID] = u.Email // Use email as username
|
|
}
|
|
|
|
// Convert to display format
|
|
logs := make([]components.AuditLogEntry, len(dbLogs))
|
|
for i, log := range dbLogs {
|
|
// Serialize details to JSON for display
|
|
detailsJSON, _ := json.Marshal(log.Details)
|
|
|
|
logs[i] = components.AuditLogEntry{
|
|
ID: log.ID,
|
|
Action: log.Action,
|
|
EntityType: log.EntityType,
|
|
EntityID: log.EntityID,
|
|
UserID: log.UserID,
|
|
Username: userMap[log.UserID],
|
|
Timestamp: log.Timestamp,
|
|
Details: log.Details,
|
|
DetailsSummary: string(detailsJSON),
|
|
}
|
|
}
|
|
|
|
// Prepare data for template
|
|
data := components.AuditLogsData{
|
|
Logs: logs,
|
|
TotalPages: totalPages,
|
|
CurrentPage: page,
|
|
TotalRecords: int(totalRecords),
|
|
FilterAction: action,
|
|
FilterEntity: entity,
|
|
FilterUser: user,
|
|
FilterDateFrom: dateFrom,
|
|
FilterDateTo: dateTo,
|
|
}
|
|
|
|
_ = components.AdminAuditLogs(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
|
|
// HandleExportAuditLogs exports audit logs as CSV
|
|
func (h *Handlers) HandleExportAuditLogs(c *gin.Context) {
|
|
// Get filter parameters from query
|
|
action := c.Query("action")
|
|
entity := c.Query("entity")
|
|
user := c.Query("user")
|
|
dateFrom := c.Query("date_from")
|
|
dateTo := c.Query("date_to")
|
|
|
|
// Initialize base query
|
|
query := h.DB.Model(&db.AuditLog{}).Order("timestamp DESC")
|
|
|
|
// Apply filters
|
|
if action != "" {
|
|
query = query.Where("action = ?", action)
|
|
}
|
|
|
|
if entity != "" {
|
|
query = query.Where("entity_type = ?", entity)
|
|
}
|
|
|
|
if user != "" {
|
|
// Join with users table to search by username
|
|
query = query.Joins("LEFT JOIN users ON audit_logs.user_id = users.id").
|
|
Where("users.email LIKE ?", "%"+user+"%")
|
|
}
|
|
|
|
if dateFrom != "" {
|
|
fromDate, err := time.Parse("2006-01-02", dateFrom)
|
|
if err == nil {
|
|
query = query.Where("timestamp >= ?", fromDate)
|
|
}
|
|
}
|
|
|
|
if dateTo != "" {
|
|
toDate, err := time.Parse("2006-01-02", dateTo)
|
|
if err == nil {
|
|
// Add one day to include the end date
|
|
toDate = toDate.Add(24 * time.Hour)
|
|
query = query.Where("timestamp < ?", toDate)
|
|
}
|
|
}
|
|
|
|
// Get all logs matching the filters
|
|
var dbLogs []db.AuditLog
|
|
if err := query.Find(&dbLogs).Error; err != nil {
|
|
h.HandleError(c, http.StatusInternalServerError, "Database error", err.Error(), err)
|
|
return
|
|
}
|
|
|
|
// Get usernames for user IDs
|
|
userIDs := make([]uint, len(dbLogs))
|
|
for i, log := range dbLogs {
|
|
userIDs[i] = log.UserID
|
|
}
|
|
|
|
var users []db.User
|
|
if err := h.DB.Where("id IN ?", userIDs).Find(&users).Error; err != nil {
|
|
h.HandleError(c, http.StatusInternalServerError, "Database error", err.Error(), err)
|
|
return
|
|
}
|
|
|
|
// Create map of user IDs to usernames
|
|
userMap := make(map[uint]string)
|
|
for _, u := range users {
|
|
userMap[u.ID] = u.Email // Use email as username
|
|
}
|
|
|
|
// Set headers for CSV download
|
|
filename := fmt.Sprintf("audit_logs_%s.csv", time.Now().Format("2006-01-02"))
|
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
|
c.Header("Content-Type", "text/csv")
|
|
|
|
// Create CSV writer
|
|
writer := csv.NewWriter(c.Writer)
|
|
|
|
// Write header row
|
|
headers := []string{"ID", "Timestamp", "Action", "Entity Type", "Entity ID", "User", "Details"}
|
|
if err := writer.Write(headers); err != nil {
|
|
h.HandleError(c, http.StatusInternalServerError, "Failed to write CSV", err.Error(), err)
|
|
return
|
|
}
|
|
|
|
// Write data rows
|
|
for _, log := range dbLogs {
|
|
// Serialize details to JSON for CSV
|
|
detailsJSON, _ := json.Marshal(log.Details)
|
|
|
|
row := []string{
|
|
fmt.Sprintf("%d", log.ID),
|
|
log.Timestamp.Format("2006-01-02 15:04:05"),
|
|
log.Action,
|
|
log.EntityType,
|
|
fmt.Sprintf("%d", log.EntityID),
|
|
userMap[log.UserID],
|
|
string(detailsJSON),
|
|
}
|
|
|
|
if err := writer.Write(row); err != nil {
|
|
// Don't stop on error, just continue
|
|
fmt.Printf("Failed to write CSV row: %v\n", err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Flush writer
|
|
writer.Flush()
|
|
|
|
if err := writer.Error(); err != nil {
|
|
fmt.Printf("Error flushing CSV writer: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// HandleSystemSettings renders the system settings page
|
|
func (h *Handlers) HandleSystemSettings(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
// In a real implementation, you'd fetch system settings from the database
|
|
_ = components.AdminLayout(ctx).Render(c.Request.Context(), c.Writer)
|
|
}
|
|
|
|
// HandleUpdateSystemSettings processes system settings updates
|
|
func (h *Handlers) HandleUpdateSystemSettings(c *gin.Context) {
|
|
// Implementation for updating system settings
|
|
// ...
|
|
c.Redirect(http.StatusFound, "/admin/settings")
|
|
}
|
|
|
|
// Helper function to handle role errors
|
|
func handleRoleError(c *gin.Context, role *db.Role, isNew bool, errorMessage string) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
data := components.RoleFormData{
|
|
Role: &components.Role{
|
|
ID: role.ID,
|
|
Name: role.Name,
|
|
Description: role.Description,
|
|
Permissions: role.GetPermissions(),
|
|
},
|
|
IsNew: isNew,
|
|
ErrorMessage: errorMessage,
|
|
AllPermissions: GetAllPermissions(),
|
|
}
|
|
_ = components.AdminRoleForm(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
|
|
// Helper function to get current user ID from context
|
|
func getUserID(c *gin.Context) uint {
|
|
user, exists := c.Get("user")
|
|
if !exists {
|
|
return 0
|
|
}
|
|
if u, ok := user.(*db.User); ok {
|
|
return u.ID
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// HandleUsers renders the user management page
|
|
func (h *Handlers) HandleUsers(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
|
|
// Fetch users from the database
|
|
var users []db.User
|
|
if err := h.DB.Find(&users).Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "list",
|
|
ErrorMessage: "Failed to fetch users: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Preload roles for each user
|
|
for i := range users {
|
|
if err := h.DB.Model(&users[i]).Association("Roles").Find(&users[i].Roles); err != nil {
|
|
// Log error but continue
|
|
fmt.Printf("Error loading roles for user %d: %v\n", users[i].ID, err)
|
|
}
|
|
}
|
|
|
|
// Get success message from flash if available
|
|
successMsg := c.Query("success")
|
|
|
|
data := components.UserManagementData{
|
|
Users: users,
|
|
ActiveTab: "list",
|
|
SuccessMessage: successMsg,
|
|
}
|
|
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
}
|
|
|
|
// HandleNewUser renders the new user creation page
|
|
func (h *Handlers) HandleNewUser(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
|
|
// Fetch available roles
|
|
var roles []db.Role
|
|
if err := h.DB.Find(&roles).Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "create",
|
|
ErrorMessage: "Failed to fetch roles: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
data := components.UserManagementData{
|
|
ActiveTab: "create",
|
|
Roles: roles,
|
|
}
|
|
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
}
|
|
|
|
// HandleCreateUser processes user creation
|
|
func (h *Handlers) HandleCreateUser(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
email := c.PostForm("email")
|
|
password := c.PostForm("password")
|
|
passwordConfirm := c.PostForm("password_confirm")
|
|
isAdmin := c.PostForm("is_admin") == "on"
|
|
roleIDs := c.PostFormArray("roles[]")
|
|
|
|
// Validate inputs
|
|
if email == "" || password == "" {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "create",
|
|
ErrorMessage: "Email and password are required",
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
if password != passwordConfirm {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "create",
|
|
ErrorMessage: "Passwords do not match",
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Create new user
|
|
user := &db.User{
|
|
Email: email,
|
|
}
|
|
|
|
// Set password (this would use a proper password hashing mechanism)
|
|
if err := user.SetPassword(password); err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "create",
|
|
ErrorMessage: "Failed to set password: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Set admin status
|
|
user.SetIsAdmin(isAdmin)
|
|
|
|
// Start transaction
|
|
tx := h.DB.Begin()
|
|
if err := tx.Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "create",
|
|
ErrorMessage: "Database error: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Save user
|
|
if err := tx.Create(user).Error; err != nil {
|
|
tx.Rollback()
|
|
data := components.UserManagementData{
|
|
ActiveTab: "create",
|
|
ErrorMessage: "Failed to create user: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Add roles if not admin and roles are selected
|
|
if !isAdmin && len(roleIDs) > 0 {
|
|
for _, roleIDStr := range roleIDs {
|
|
roleID, err := strconv.ParseUint(roleIDStr, 10, 32)
|
|
if err != nil {
|
|
continue // Skip invalid IDs
|
|
}
|
|
|
|
// Check if role exists
|
|
var role db.Role
|
|
if err := tx.First(&role, roleID).Error; err != nil {
|
|
continue // Skip if role doesn't exist
|
|
}
|
|
|
|
// Add role to user
|
|
if err := tx.Model(user).Association("Roles").Append(&role); err != nil {
|
|
tx.Rollback()
|
|
data := components.UserManagementData{
|
|
ActiveTab: "create",
|
|
ErrorMessage: "Failed to assign roles: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create audit log entry
|
|
creatorID := getUserID(c)
|
|
auditLog := &db.AuditLog{
|
|
Action: "create",
|
|
EntityType: "user",
|
|
EntityID: user.ID,
|
|
UserID: creatorID,
|
|
Timestamp: time.Now(),
|
|
Details: map[string]interface{}{
|
|
"email": user.Email,
|
|
"is_admin": user.GetIsAdmin(),
|
|
},
|
|
}
|
|
|
|
if err := tx.Create(auditLog).Error; err != nil {
|
|
tx.Rollback()
|
|
data := components.UserManagementData{
|
|
ActiveTab: "create",
|
|
ErrorMessage: "Failed to create audit log: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Commit transaction
|
|
if err := tx.Commit().Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "create",
|
|
ErrorMessage: "Failed to commit transaction: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// For HTMX requests, render the user list with success message
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
// Fetch users from the database
|
|
var users []db.User
|
|
if err := h.DB.Find(&users).Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "list",
|
|
ErrorMessage: "Failed to fetch users after creation: " + err.Error(),
|
|
}
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
return
|
|
}
|
|
|
|
// Preload roles for each user
|
|
for i := range users {
|
|
if err := h.DB.Model(&users[i]).Association("Roles").Find(&users[i].Roles); err != nil {
|
|
// Log error but continue
|
|
fmt.Printf("Error loading roles for user %d: %v\n", users[i].ID, err)
|
|
}
|
|
}
|
|
|
|
data := components.UserManagementData{
|
|
Users: users,
|
|
ActiveTab: "list",
|
|
SuccessMessage: "User created successfully",
|
|
}
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
// Redirect to user list with success message for regular requests
|
|
c.Redirect(http.StatusFound, "/admin/users?success=User+created+successfully")
|
|
}
|
|
}
|
|
|
|
// HandleEditUser renders the user edit page
|
|
func (h *Handlers) HandleEditUser(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
userID := c.Param("id")
|
|
|
|
// Fetch user
|
|
var user db.User
|
|
if err := h.DB.First(&user, userID).Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "list",
|
|
ErrorMessage: "User not found",
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Fetch user's roles
|
|
if err := h.DB.Model(&user).Association("Roles").Find(&user.Roles); err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Failed to fetch user roles: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Fetch all available roles
|
|
var roles []db.Role
|
|
if err := h.DB.Find(&roles).Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Failed to fetch roles: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
Roles: roles,
|
|
}
|
|
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
}
|
|
|
|
// HandleUpdateUser processes user updates
|
|
func (h *Handlers) HandleUpdateUser(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
userID := c.Param("id")
|
|
email := c.PostForm("email")
|
|
password := c.PostForm("password")
|
|
passwordConfirm := c.PostForm("password_confirm")
|
|
isAdmin := c.PostForm("is_admin") == "on"
|
|
accountLocked := c.PostForm("account_locked") == "on"
|
|
roleIDs := c.PostFormArray("roles[]")
|
|
|
|
// Fetch existing user
|
|
var user db.User
|
|
if err := h.DB.First(&user, userID).Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "list",
|
|
ErrorMessage: "User not found",
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check if trying to update the last admin
|
|
if user.GetIsAdmin() && !isAdmin {
|
|
// Count how many admins there are
|
|
var adminCount int64
|
|
if err := h.DB.Model(&db.User{}).Where("metadata->>'is_admin' = 'true'").Count(&adminCount).Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Failed to check admin count: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// If this is the last admin, prevent removal of admin status
|
|
if adminCount <= 1 {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Cannot remove admin status from the last administrator",
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// Start transaction
|
|
tx := h.DB.Begin()
|
|
if err := tx.Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Database error: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Update email
|
|
if email != "" && email != user.Email {
|
|
user.Email = email
|
|
}
|
|
|
|
// Update password if provided
|
|
if password != "" {
|
|
if password != passwordConfirm {
|
|
tx.Rollback()
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Passwords do not match",
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := user.SetPassword(password); err != nil {
|
|
tx.Rollback()
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Failed to set password: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// Update admin status and account lock status
|
|
user.SetIsAdmin(isAdmin)
|
|
user.SetAccountLocked(accountLocked)
|
|
|
|
// Save user changes
|
|
if err := tx.Save(&user).Error; err != nil {
|
|
tx.Rollback()
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Failed to update user: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Update roles if not admin
|
|
if !isAdmin {
|
|
// First, remove all existing roles
|
|
if err := tx.Model(&user).Association("Roles").Clear(); err != nil {
|
|
tx.Rollback()
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Failed to clear existing roles: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Then add the selected roles
|
|
if len(roleIDs) > 0 {
|
|
for _, roleIDStr := range roleIDs {
|
|
roleID, err := strconv.ParseUint(roleIDStr, 10, 32)
|
|
if err != nil {
|
|
continue // Skip invalid IDs
|
|
}
|
|
|
|
// Check if role exists
|
|
var role db.Role
|
|
if err := tx.First(&role, roleID).Error; err != nil {
|
|
continue // Skip if role doesn't exist
|
|
}
|
|
|
|
// Add role to user
|
|
if err := tx.Model(&user).Association("Roles").Append(&role); err != nil {
|
|
tx.Rollback()
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Failed to assign roles: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create audit log entry
|
|
updaterID := getUserID(c)
|
|
auditLog := &db.AuditLog{
|
|
Action: "update",
|
|
EntityType: "user",
|
|
EntityID: user.ID,
|
|
UserID: updaterID,
|
|
Timestamp: time.Now(),
|
|
Details: map[string]interface{}{
|
|
"email": user.Email,
|
|
"is_admin": user.GetIsAdmin(),
|
|
"account_locked": user.GetAccountLocked(),
|
|
"password_changed": password != "",
|
|
},
|
|
}
|
|
|
|
if err := tx.Create(auditLog).Error; err != nil {
|
|
tx.Rollback()
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Failed to create audit log: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Commit transaction
|
|
if err := tx.Commit().Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "edit",
|
|
EditUser: &user,
|
|
ErrorMessage: "Failed to commit transaction: " + err.Error(),
|
|
}
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
_ = components.UserManagement(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
return
|
|
}
|
|
|
|
// For HTMX requests, render the user list with success message
|
|
if c.GetHeader("HX-Request") == "true" {
|
|
// Fetch users from the database
|
|
var users []db.User
|
|
if err := h.DB.Find(&users).Error; err != nil {
|
|
data := components.UserManagementData{
|
|
ActiveTab: "list",
|
|
ErrorMessage: "Failed to fetch users after update: " + err.Error(),
|
|
}
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
return
|
|
}
|
|
|
|
// Preload roles for each user
|
|
for i := range users {
|
|
if err := h.DB.Model(&users[i]).Association("Roles").Find(&users[i].Roles); err != nil {
|
|
// Log error but continue
|
|
fmt.Printf("Error loading roles for user %d: %v\n", users[i].ID, err)
|
|
}
|
|
}
|
|
|
|
data := components.UserManagementData{
|
|
Users: users,
|
|
ActiveTab: "list",
|
|
SuccessMessage: "User updated successfully",
|
|
}
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
} else {
|
|
// Redirect to user list with success message for regular requests
|
|
c.Redirect(http.StatusFound, "/admin/users?success=User+updated+successfully")
|
|
}
|
|
}
|
|
|
|
// HandleDeleteUser processes user deletion
|
|
func (h *Handlers) HandleDeleteUser(c *gin.Context) {
|
|
userID := c.Param("id")
|
|
|
|
// Fetch user
|
|
var user db.User
|
|
if err := h.DB.First(&user, userID).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
|
return
|
|
}
|
|
|
|
// Check if this is an admin user
|
|
if user.GetIsAdmin() {
|
|
// Check if other administrators exist
|
|
var otherAdminCount int64
|
|
// Use the actual 'is_admin' column, comparing against true
|
|
if err := h.DB.Model(&db.User{}).
|
|
Where("is_admin = ? AND id != ?", true, user.ID).
|
|
Count(&otherAdminCount).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check for other administrators"})
|
|
return
|
|
}
|
|
|
|
// If no other administrators exist, prevent deletion
|
|
if otherAdminCount == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete the last administrator"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check if trying to delete yourself
|
|
currentUserID := getUserID(c)
|
|
if currentUserID == user.ID {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete your own account"})
|
|
return
|
|
}
|
|
|
|
// Start transaction
|
|
tx := h.DB.Begin()
|
|
if err := tx.Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
|
return
|
|
}
|
|
|
|
// Create audit log entry before deletion
|
|
auditLog := &db.AuditLog{
|
|
Action: "delete",
|
|
EntityType: "user",
|
|
EntityID: user.ID,
|
|
UserID: currentUserID,
|
|
Timestamp: time.Now(),
|
|
Details: map[string]interface{}{
|
|
"email": user.Email,
|
|
"is_admin": user.GetIsAdmin(),
|
|
"account_locked": user.GetAccountLocked(),
|
|
},
|
|
}
|
|
|
|
if err := tx.Create(auditLog).Error; err != nil {
|
|
tx.Rollback()
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create audit log"})
|
|
return
|
|
}
|
|
|
|
// Remove roles association
|
|
if err := tx.Model(&user).Association("Roles").Clear(); err != nil {
|
|
tx.Rollback()
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear user roles"})
|
|
return
|
|
}
|
|
|
|
// Delete user
|
|
if err := tx.Delete(&user).Error; err != nil {
|
|
tx.Rollback()
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"})
|
|
return
|
|
}
|
|
|
|
// Commit transaction
|
|
if err := tx.Commit().Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction"})
|
|
return
|
|
}
|
|
|
|
// Fetch the updated user list to return
|
|
ctx := components.CreateTemplateContext(c)
|
|
var users []db.User
|
|
if err := h.DB.Find(&users).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch updated user list"})
|
|
return
|
|
}
|
|
|
|
// Preload roles for each user
|
|
for i := range users {
|
|
if err := h.DB.Model(&users[i]).Association("Roles").Find(&users[i].Roles); err != nil {
|
|
// Log error but continue
|
|
fmt.Printf("Error loading roles for user %d: %v\n", users[i].ID, err)
|
|
}
|
|
}
|
|
|
|
// Render the updated user list
|
|
data := components.UserManagementData{
|
|
Users: users,
|
|
ActiveTab: "list",
|
|
SuccessMessage: "User deleted successfully",
|
|
}
|
|
|
|
// Always use the partial for HTMX delete requests
|
|
_ = components.UserManagementContent(data).Render(ctx, c.Writer)
|
|
}
|
|
|
|
// HandleLogViewer renders the log viewer page
|
|
func (h *Handlers) HandleLogViewer(c *gin.Context) {
|
|
ctx := components.CreateTemplateContext(c)
|
|
|
|
// Create logs data for initial page load
|
|
logFilePath := filepath.Join(h.LogsDir, "scheduler.log")
|
|
|
|
// Log the full path for debugging
|
|
log.Printf("Log viewer initialized with log file path: %s", logFilePath)
|
|
|
|
data := components.LogViewerData{
|
|
Logs: []components.LogEntry{},
|
|
CurrentFilter: "",
|
|
LogFilePath: logFilePath,
|
|
}
|
|
|
|
// Render the log viewer component
|
|
components.AdminLogs(ctx, data).Render(ctx, c.Writer)
|
|
}
|
|
|
|
// HandleLogStream handles WebSocket connections for real-time log streaming
|
|
func (h *Handlers) HandleLogStream(c *gin.Context) {
|
|
// Configure upgrader
|
|
upgrader := websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
return true // Allow all origins for now
|
|
},
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] New WebSocket connection request from %s\n", c.ClientIP())
|
|
|
|
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Failed to upgrade WebSocket for %s: %v\n", c.ClientIP(), err)
|
|
return
|
|
}
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] WebSocket connection upgraded for %s\n", c.ClientIP())
|
|
|
|
// Set ping handler to respond with pong
|
|
ws.SetPingHandler(func(data string) error {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Received ping from %s, responding with pong\n", ws.RemoteAddr())
|
|
return ws.WriteControl(websocket.PongMessage, []byte{}, time.Now().Add(5*time.Second))
|
|
})
|
|
|
|
// Ensure connection is closed eventually
|
|
defer func() {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Closing WebSocket connection for %s\n", ws.RemoteAddr())
|
|
ws.Close()
|
|
}()
|
|
|
|
// Register the new client and create its mutex
|
|
WebSocketClientsMutex.Lock()
|
|
WebSocketClients[ws] = true
|
|
WebSocketClientWriteMutexes[ws] = &sync.Mutex{}
|
|
numClients := len(WebSocketClients)
|
|
WebSocketClientsMutex.Unlock()
|
|
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Registered client %s. Total clients: %d\n", ws.RemoteAddr(), numClients)
|
|
|
|
// De-register the client when the handler exits
|
|
defer func() {
|
|
WebSocketClientsMutex.Lock()
|
|
delete(WebSocketClients, ws)
|
|
delete(WebSocketClientWriteMutexes, ws)
|
|
remainingClients := len(WebSocketClients)
|
|
WebSocketClientsMutex.Unlock()
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] De-registered client %s. Remaining clients: %d\n", ws.RemoteAddr(), remainingClients)
|
|
}()
|
|
|
|
// Send recent logs immediately after connection
|
|
h.sendRecentLogs(ws)
|
|
|
|
// Start a goroutine to send pings periodically to keep the connection alive
|
|
stopPinger := make(chan struct{})
|
|
go func() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Failed to send ping to client %s: %v\n", ws.RemoteAddr(), err)
|
|
return
|
|
}
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Sent ping to client %s\n", ws.RemoteAddr())
|
|
case <-stopPinger:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Keep the connection alive by reading messages (and discarding them)
|
|
// This also detects when the client closes the connection.
|
|
for {
|
|
messageType, message, err := ws.ReadMessage()
|
|
if err != nil {
|
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] WebSocket closed unexpectedly for %s: %v\n", ws.RemoteAddr(), err)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] WebSocket closed normally for %s.\n", ws.RemoteAddr())
|
|
}
|
|
break // Exit loop on Read error
|
|
}
|
|
|
|
// Handle client messages (like ping)
|
|
if messageType == websocket.TextMessage && len(message) > 0 {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Received message from client %s: %s\n", ws.RemoteAddr(), message)
|
|
|
|
// Try to parse as JSON and check for ping
|
|
var msgData map[string]interface{}
|
|
if err := json.Unmarshal(message, &msgData); err == nil {
|
|
if msgType, ok := msgData["type"].(string); ok && msgType == "ping" {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Received ping from client %s, responding with pong\n", ws.RemoteAddr())
|
|
|
|
// Send a pong response
|
|
pongResp := map[string]interface{}{
|
|
"type": "pong",
|
|
"time": time.Now().Unix(),
|
|
}
|
|
|
|
WebSocketClientsMutex.Lock()
|
|
mutex, exists := WebSocketClientWriteMutexes[ws]
|
|
WebSocketClientsMutex.Unlock()
|
|
|
|
if exists {
|
|
mutex.Lock()
|
|
err := ws.WriteJSON(pongResp)
|
|
mutex.Unlock()
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Error sending pong to client %s: %v\n", ws.RemoteAddr(), err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop the ping goroutine
|
|
close(stopPinger)
|
|
}
|
|
|
|
// sendRecentLogs sends recent log entries to a new WebSocket client
|
|
func (h *Handlers) sendRecentLogs(ws *websocket.Conn) {
|
|
// Get the mutex for this client *first*
|
|
WebSocketClientsMutex.Lock()
|
|
mutex, exists := WebSocketClientWriteMutexes[ws]
|
|
if !exists {
|
|
// This shouldn't happen if HandleLogStream is correct, but handle defensively
|
|
WebSocketClientsMutex.Unlock()
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Mutex not found for client %v during sendRecentLogs. Aborting recent logs send.\n", ws.RemoteAddr())
|
|
return
|
|
}
|
|
WebSocketClientsMutex.Unlock()
|
|
|
|
// Construct the path to the log file
|
|
logFilePath := filepath.Join(h.LogsDir, "scheduler.log")
|
|
|
|
// Check if the log file exists
|
|
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Log file not found at %s for sendRecentLogs. Sending example logs.\n", logFilePath)
|
|
h.sendExampleLogs(ws) // Send examples if main log file isn't there
|
|
return
|
|
}
|
|
|
|
// Open the log file
|
|
file, err := os.Open(logFilePath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Error opening log file %s: %v. Sending example logs.\n", logFilePath, err)
|
|
h.sendExampleLogs(ws)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
// Read the last 20 lines
|
|
lines, err := readLastLines(file, 20)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Error reading log file %s: %v. Sending example logs.\n", logFilePath, err)
|
|
h.sendExampleLogs(ws)
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Read %d lines from %s for client %v.\n", len(lines), logFilePath, ws.RemoteAddr())
|
|
|
|
// Parse and send each line as a log entry, protected by the client's mutex
|
|
for _, line := range lines {
|
|
level, source, message := parseLogLine(line)
|
|
timestamp := extractTimestamp(line)
|
|
|
|
logEntry := components.LogEntry{
|
|
Timestamp: timestamp,
|
|
Level: level,
|
|
Message: message,
|
|
Source: source,
|
|
}
|
|
|
|
// Use the specific client's mutex
|
|
// fmt.Fprintf(os.Stderr, "[DEBUG-WS] Sending recent log %d/%d to client %v\n", i+1, len(lines), ws.RemoteAddr())
|
|
mutex.Lock()
|
|
err := ws.WriteJSON(logEntry)
|
|
mutex.Unlock()
|
|
|
|
if err != nil {
|
|
// fmt.Fprintf(os.Stderr, "[DEBUG-WS] Error sending recent log %d to client %v: %v. Stopping recent logs send.\n", i+1, ws.RemoteAddr(), err)
|
|
// Don't try to remove the client here, let the main read loop handle it
|
|
break // Stop sending recent logs on first error
|
|
}
|
|
}
|
|
// fmt.Fprintf(os.Stderr, "[DEBUG-WS] Finished sending %d recent logs to client %v.\n", len(lines), ws.RemoteAddr())
|
|
}
|
|
|
|
// readLastLines reads the last n lines from a file
|
|
func readLastLines(file *os.File, n int) ([]string, error) {
|
|
// Implement a simpler version that reads the whole file and keeps the last n lines
|
|
scanner := bufio.NewScanner(file)
|
|
var lines []string
|
|
|
|
// Read all lines
|
|
for scanner.Scan() {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return the last n lines (or all if less than n)
|
|
if len(lines) <= n {
|
|
return lines, nil
|
|
}
|
|
|
|
return lines[len(lines)-n:], nil
|
|
}
|
|
|
|
// parseLogLine extracts level, source, and message from a log line
|
|
// This is specific to the format found in the log file being read,
|
|
// NOT the format generated by the standard Go logger directly.
|
|
func parseLogLine(line string) (level, source, message string) {
|
|
// Default values
|
|
level = "info"
|
|
source = "system"
|
|
originalLine := line // Keep original for prefix check
|
|
|
|
// Check for level prefixes first
|
|
foundPrefix := false
|
|
if strings.HasPrefix(originalLine, "DEBUG:") { // Check original line for prefix
|
|
level = "debug"
|
|
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "DEBUG:"))
|
|
foundPrefix = true
|
|
} else if strings.HasPrefix(originalLine, "INFO:") {
|
|
level = "info"
|
|
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "INFO:"))
|
|
foundPrefix = true
|
|
} else if strings.HasPrefix(originalLine, "ERROR:") {
|
|
level = "error"
|
|
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "ERROR:"))
|
|
foundPrefix = true
|
|
} else if strings.HasPrefix(originalLine, "WARN:") {
|
|
level = "warn"
|
|
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "WARN:"))
|
|
foundPrefix = true
|
|
} else if strings.HasPrefix(originalLine, "WARNING:") {
|
|
level = "warn"
|
|
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "WARNING:"))
|
|
foundPrefix = true
|
|
} else if strings.HasPrefix(originalLine, "FATAL:") {
|
|
level = "fatal"
|
|
line = strings.TrimSpace(strings.TrimPrefix(originalLine, "FATAL:"))
|
|
foundPrefix = true
|
|
}
|
|
|
|
// Now parse the rest (timestamp + message) using the potentially modified 'line'
|
|
parts := strings.SplitN(line, " ", 3)
|
|
if len(parts) >= 3 {
|
|
message = parts[2] // The rest is the message
|
|
|
|
// Try to extract source *only if no level prefix was found initially*
|
|
// Assumes standard log format prefixes message with file:line
|
|
if !foundPrefix {
|
|
if fileStart := strings.Index(message, " "); fileStart > 0 {
|
|
filePath := message[:fileStart]
|
|
if strings.Contains(filePath, ":") {
|
|
filePathParts := strings.Split(filePath, "/")
|
|
if len(filePathParts) > 0 {
|
|
fileNameWithLine := filePathParts[len(filePathParts)-1]
|
|
fileName := strings.Split(fileNameWithLine, ":")[0]
|
|
source = strings.TrimSuffix(fileName, ".go")
|
|
}
|
|
}
|
|
// Update message to remove the file info
|
|
message = message[fileStart+1:]
|
|
}
|
|
}
|
|
|
|
// If no prefix was found, attempt level detection from message content (e.g., [info])
|
|
if !foundPrefix {
|
|
parsedLevel, cleanMessage := parseLevelFromMessageContent(message)
|
|
level = parsedLevel
|
|
message = cleanMessage
|
|
}
|
|
|
|
} else {
|
|
// Fallback if split doesn't work as expected, use the (potentially prefix-stripped) line
|
|
message = line
|
|
}
|
|
|
|
return level, source, message
|
|
}
|
|
|
|
// parseLevelFromMessageContent checks for bracketed level indicators
|
|
func parseLevelFromMessageContent(msg string) (string, string) {
|
|
level := "info" // Default
|
|
cleanMsg := msg
|
|
|
|
if strings.HasPrefix(msg, "[debug]") {
|
|
level = "debug"
|
|
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[debug]"))
|
|
} else if strings.HasPrefix(msg, "[info]") {
|
|
level = "info"
|
|
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[info]"))
|
|
} else if strings.HasPrefix(msg, "[warn]") {
|
|
level = "warn"
|
|
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[warn]"))
|
|
} else if strings.HasPrefix(msg, "[warning]") {
|
|
level = "warn"
|
|
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[warning]"))
|
|
} else if strings.HasPrefix(msg, "[error]") {
|
|
level = "error"
|
|
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[error]"))
|
|
} else if strings.HasPrefix(msg, "[fatal]") {
|
|
level = "fatal"
|
|
cleanMsg = strings.TrimSpace(strings.TrimPrefix(msg, "[fatal]"))
|
|
}
|
|
// Optional: Add inference based on keywords like the logger does
|
|
// else if strings.Contains(strings.ToLower(msg), "error") { level = "error" } ...
|
|
return level, cleanMsg
|
|
}
|
|
|
|
// extractTimestamp extracts the timestamp from a log line, handling potential prefixes
|
|
func extractTimestamp(line string) time.Time {
|
|
now := time.Now() // Default
|
|
originalLine := line
|
|
|
|
// Remove known level prefixes for timestamp parsing
|
|
prefixes := []string{"DEBUG:", "INFO:", "ERROR:", "WARN:", "WARNING:", "FATAL:"}
|
|
for _, prefix := range prefixes {
|
|
if strings.HasPrefix(line, prefix) {
|
|
line = strings.TrimSpace(strings.TrimPrefix(line, prefix))
|
|
break
|
|
}
|
|
}
|
|
|
|
// Try to extract timestamp parts (date and time)
|
|
parts := strings.SplitN(line, " ", 3)
|
|
if len(parts) >= 2 {
|
|
dateStr := parts[0]
|
|
timeStr := parts[1]
|
|
timestampStr := dateStr + " " + timeStr
|
|
|
|
// List of timestamp formats to try
|
|
formats := []string{
|
|
"2006/01/02 15:04:05", // Standard Go log with slashes
|
|
"2006/01/02 15:04:05.999", // With milliseconds
|
|
"2006/01/02 15:04:05.999999", // With microseconds
|
|
"2006-01-02 15:04:05", // Standard Go log with dashes
|
|
"2006-01-02 15:04:05.999", // With milliseconds
|
|
"2006-01-02 15:04:05.999999", // With microseconds
|
|
}
|
|
|
|
for _, format := range formats {
|
|
timestamp, err := time.Parse(format, timestampStr)
|
|
if err == nil {
|
|
return timestamp // Successfully parsed
|
|
}
|
|
}
|
|
// If all formats failed, log the original attempt
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-TIMESTAMP] Failed to parse timestamp from '%s' (derived from line: %s)\n", timestampStr, originalLine)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-TIMESTAMP] Could not split timestamp parts from line: %s\n", originalLine)
|
|
}
|
|
|
|
return now // Return current time if parsing failed
|
|
}
|
|
|
|
// sendExampleLogs sends example log entries for demonstration
|
|
func (h *Handlers) sendExampleLogs(ws *websocket.Conn) {
|
|
// Get the mutex for this client *first*
|
|
WebSocketClientsMutex.Lock()
|
|
mutex, exists := WebSocketClientWriteMutexes[ws]
|
|
if !exists {
|
|
WebSocketClientsMutex.Unlock()
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Mutex not found for client %v during sendExampleLogs. Aborting example logs send.\n", ws.RemoteAddr())
|
|
return
|
|
}
|
|
WebSocketClientsMutex.Unlock()
|
|
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Sending example logs to client %v\n", ws.RemoteAddr())
|
|
|
|
// Example log entries for demonstration
|
|
exampleLogs := []components.LogEntry{
|
|
{
|
|
Timestamp: time.Now().UTC().Add(-time.Minute * 5),
|
|
Level: "info",
|
|
Message: "Application started successfully",
|
|
Source: "main",
|
|
},
|
|
{
|
|
Timestamp: time.Now().UTC().Add(-time.Minute * 3),
|
|
Level: "debug",
|
|
Message: "Connected to database",
|
|
Source: "database",
|
|
},
|
|
{
|
|
Timestamp: time.Now().UTC().Add(-time.Minute * 2),
|
|
Level: "warn",
|
|
Message: "High memory usage detected: 85%",
|
|
Source: "monitor",
|
|
},
|
|
}
|
|
|
|
for i, logEntry := range exampleLogs {
|
|
// Use the specific client's mutex
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Sending example log %d/%d to client %v\n", i+1, len(exampleLogs), ws.RemoteAddr())
|
|
mutex.Lock()
|
|
err := ws.WriteJSON(logEntry)
|
|
mutex.Unlock()
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Error sending example log %d to client %v: %v. Stopping example logs send.\n", i+1, ws.RemoteAddr(), err)
|
|
break // Stop sending example logs on first error
|
|
}
|
|
}
|
|
fmt.Fprintf(os.Stderr, "[DEBUG-WS] Finished sending example logs to client %v.\n", ws.RemoteAddr())
|
|
}
|