feat: add chat log admin management and refactor admin panel to lazy-load tab content
This commit is contained in:
1 parent
f650a3a94e
commit
74133ec589
12 files changed
+921
-387
No files matched your search
@@ -156,6 +156,24 @@ type AdminUsersResponse struct {
|
||||
GeneratedAt string `json:"generatedAt"`
|
||||
}
|
||||
|
||||
type ChatLogEntry struct {
|
||||
ID uint `json:"id"`
|
||||
Room string `json:"room"`
|
||||
Username string `json:"username"`
|
||||
Text string `json:"text"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type ChatLogListResponse struct {
|
||||
Entries []ChatLogEntry `json:"entries"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type ChatLogBulkDeleteRequest struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
|
||||
type ScheduleEntry struct {
|
||||
Index int `json:"index"`
|
||||
Path string `json:"path"`
|
||||
@@ -1934,6 +1952,162 @@ func handleModerationCommand(msg Message, conn *websocket.Conn) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleAdminChatLog lists chat log entries with optional filtering and pagination
|
||||
func handleAdminChatLog(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
room := strings.TrimSpace(query.Get("room"))
|
||||
|
||||
limit := 50
|
||||
if rawLimit := strings.TrimSpace(query.Get("limit")); rawLimit != "" {
|
||||
parsedLimit, err := strconv.Atoi(rawLimit)
|
||||
if err != nil || parsedLimit <= 0 {
|
||||
http.Error(w, "invalid limit", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if parsedLimit > 200 {
|
||||
parsedLimit = 200
|
||||
}
|
||||
limit = parsedLimit
|
||||
}
|
||||
|
||||
dbQuery := db.Model(&Message{})
|
||||
|
||||
if room != "" {
|
||||
dbQuery = dbQuery.Where("room = ?", room)
|
||||
}
|
||||
|
||||
if beforeRaw := strings.TrimSpace(query.Get("before")); beforeRaw != "" {
|
||||
beforeTime, err := time.Parse(time.RFC3339, beforeRaw)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid before timestamp, use RFC3339 format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
dbQuery = dbQuery.Where("timestamp < ?", beforeTime)
|
||||
}
|
||||
|
||||
if username := strings.TrimSpace(query.Get("username")); username != "" {
|
||||
dbQuery = dbQuery.Where("username LIKE ?", "%"+username+"%")
|
||||
}
|
||||
|
||||
if text := strings.TrimSpace(query.Get("text")); text != "" {
|
||||
dbQuery = dbQuery.Where("text LIKE ?", "%"+text+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := dbQuery.Count(&total).Error; err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to count chat log entries: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var messages []Message
|
||||
if err := dbQuery.Order("timestamp desc").Limit(limit).Find(&messages).Error; err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to load chat log entries: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
entries := make([]ChatLogEntry, len(messages))
|
||||
for i, msg := range messages {
|
||||
entries[i] = ChatLogEntry{
|
||||
ID: msg.ID,
|
||||
Room: msg.Room,
|
||||
Username: msg.Username,
|
||||
Text: msg.Text,
|
||||
Timestamp: msg.Timestamp,
|
||||
Type: msg.Type,
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(ChatLogListResponse{Entries: entries, Total: total})
|
||||
}
|
||||
|
||||
// handleAdminChatLogByID returns or deletes a single chat log entry
|
||||
func handleAdminChatLogByID(w http.ResponseWriter, r *http.Request) {
|
||||
idPart := strings.TrimPrefix(r.URL.Path, "/api/admin/chat-log/")
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
if idPart == "" || strings.Contains(idPart, "/") {
|
||||
http.Error(w, "invalid chat log id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(idPart, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
http.Error(w, "invalid chat log id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
var msg Message
|
||||
if err := db.First(&msg, id).Error; err != nil {
|
||||
http.Error(w, "chat log entry not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(ChatLogEntry{
|
||||
ID: msg.ID,
|
||||
Room: msg.Room,
|
||||
Username: msg.Username,
|
||||
Text: msg.Text,
|
||||
Timestamp: msg.Timestamp,
|
||||
Type: msg.Type,
|
||||
})
|
||||
case http.MethodDelete:
|
||||
result := db.Delete(&Message{}, id)
|
||||
if result.Error != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to delete chat log entry: %v", result.Error), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
http.Error(w, "chat log entry not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAdminChatLogBulk deletes multiple chat log entries by ID
|
||||
func handleAdminChatLogBulk(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var payload ChatLogBulkDeleteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid request body: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(payload.IDs) == 0 {
|
||||
http.Error(w, "ids array is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(payload.IDs) > 500 {
|
||||
http.Error(w, "maximum 500 ids per bulk request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
result := db.Delete(&Message{}, payload.IDs)
|
||||
if result.Error != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to bulk delete chat log entries: %v", result.Error), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{"deleted": result.RowsAffected})
|
||||
}
|
||||
|
||||
func main() {
|
||||
initRedis()
|
||||
initDB()
|
||||
@@ -1954,6 +2128,9 @@ func main() {
|
||||
http.HandleFunc("/api/admin/media/uploads", requireAdminAuth(handleAdminMediaUploads))
|
||||
http.HandleFunc("/api/admin/media/uploads/", requireAdminAuth(handleAdminMediaUploads))
|
||||
http.HandleFunc("/api/admin/media/", requireAdminAuth(handleAdminMediaByID))
|
||||
http.HandleFunc("/api/admin/chat-log/bulk", requireAdminAuth(handleAdminChatLogBulk))
|
||||
http.HandleFunc("/api/admin/chat-log/", requireAdminAuth(handleAdminChatLogByID))
|
||||
http.HandleFunc("/api/admin/chat-log", requireAdminAuth(handleAdminChatLog))
|
||||
http.HandleFunc("/metrics", handleMetrics)
|
||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1961,6 +2138,13 @@ func main() {
|
||||
fmt.Fprint(w, `{"status":"healthy","service":"chat","timestamp":"`+time.Now().UTC().Format(time.RFC3339)+`"}`)
|
||||
})
|
||||
|
||||
// Catch-all: return 404 JSON for all unhandled paths
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprint(w, `{"error":"not found"}`)
|
||||
})
|
||||
|
||||
port := os.Getenv("CHAT_PORT")
|
||||
if port == "" {
|
||||
port = "3000"
|
||||
|
||||
Reference in new issue
Block a user