355 lines
8.8 KiB
Go
355 lines
8.8 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"hightube/internal/chat"
|
|
"hightube/internal/db"
|
|
"hightube/internal/model"
|
|
"hightube/internal/monitor"
|
|
"hightube/internal/stream"
|
|
"hightube/internal/utils"
|
|
)
|
|
|
|
var adminRTMP *stream.RTMPServer
|
|
|
|
func BindAdminDependencies(rtmpSrv *stream.RTMPServer) {
|
|
adminRTMP = rtmpSrv
|
|
}
|
|
|
|
func AdminLogin(c *gin.Context) {
|
|
var req LoginRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var user model.User
|
|
if err := db.DB.Where("username = ?", strings.TrimSpace(req.Username)).First(&user).Error; err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
|
|
return
|
|
}
|
|
|
|
if !utils.CheckPasswordHash(req.Password, user.Password) {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
|
|
return
|
|
}
|
|
|
|
if !user.Enabled {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Account is disabled"})
|
|
return
|
|
}
|
|
|
|
if user.Role != "admin" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
|
|
return
|
|
}
|
|
|
|
token, err := utils.GenerateToken(user.ID, user.Username, user.Role)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create admin session"})
|
|
return
|
|
}
|
|
|
|
c.SetCookie(adminSessionCookieName, token, 86400, "/", "", false, true)
|
|
monitor.Auditf("admin=%s signed in", user.Username)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"username": user.Username,
|
|
"role": user.Role,
|
|
})
|
|
}
|
|
|
|
func AdminLogout(c *gin.Context) {
|
|
operator, _ := c.Get("username")
|
|
c.SetCookie(adminSessionCookieName, "", -1, "/", "", false, true)
|
|
monitor.Auditf("admin=%v signed out", operator)
|
|
c.JSON(http.StatusOK, gin.H{"message": "signed out"})
|
|
}
|
|
|
|
func GetAdminSession(c *gin.Context) {
|
|
username, _ := c.Get("username")
|
|
role, _ := c.Get("role")
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"username": username,
|
|
"role": role,
|
|
})
|
|
}
|
|
|
|
func GetAdminOverview(c *gin.Context) {
|
|
stats := monitor.GetSnapshot()
|
|
chatStats := chat.StatsSnapshot{}
|
|
if chat.MainHub != nil {
|
|
chatStats = chat.MainHub.GetStatsSnapshot()
|
|
}
|
|
|
|
activeCount := 0
|
|
activePaths := []string{}
|
|
if adminRTMP != nil {
|
|
activeCount = adminRTMP.ActiveStreamCount()
|
|
activePaths = adminRTMP.ActiveStreamPaths()
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"system": stats,
|
|
"stream": gin.H{
|
|
"active_stream_count": activeCount,
|
|
"active_stream_paths": activePaths,
|
|
},
|
|
"chat": chatStats,
|
|
})
|
|
}
|
|
|
|
func GetAdminHealth(c *gin.Context) {
|
|
type dbHealth struct {
|
|
OK bool `json:"ok"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
health := gin.H{
|
|
"api": true,
|
|
"rtmp": adminRTMP != nil,
|
|
}
|
|
|
|
dbOK := dbHealth{OK: true}
|
|
if err := db.DB.Exec("SELECT 1").Error; err != nil {
|
|
dbOK.OK = false
|
|
dbOK.Error = err.Error()
|
|
}
|
|
health["db"] = dbOK
|
|
|
|
c.JSON(http.StatusOK, health)
|
|
}
|
|
|
|
func ListAdminLogs(c *gin.Context) {
|
|
level := c.Query("level")
|
|
keyword := c.Query("keyword")
|
|
page := parseIntWithDefault(c.Query("page"), 1)
|
|
pageSize := parseIntWithDefault(c.Query("page_size"), 20)
|
|
|
|
items, total := monitor.Query(level, keyword, page, pageSize)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"items": items,
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": pageSize,
|
|
})
|
|
}
|
|
|
|
func StreamAdminLogs(c *gin.Context) {
|
|
c.Header("Content-Type", "text/event-stream")
|
|
c.Header("Cache-Control", "no-cache")
|
|
c.Header("Connection", "keep-alive")
|
|
c.Header("X-Accel-Buffering", "no")
|
|
|
|
flusher, ok := c.Writer.(http.Flusher)
|
|
if !ok {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "streaming unsupported"})
|
|
return
|
|
}
|
|
|
|
ch := monitor.Subscribe()
|
|
defer monitor.Unsubscribe(ch)
|
|
|
|
for {
|
|
select {
|
|
case entry := <-ch:
|
|
payload, _ := json.Marshal(entry)
|
|
_, _ = c.Writer.Write([]byte("event: log\n"))
|
|
_, _ = c.Writer.Write([]byte("data: " + string(payload) + "\n\n"))
|
|
flusher.Flush()
|
|
case <-c.Request.Context().Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type updateRoleRequest struct {
|
|
Role string `json:"role" binding:"required"`
|
|
}
|
|
|
|
func UpdateUserRole(c *gin.Context) {
|
|
userID, ok := parsePathUint(c.Param("id"))
|
|
if !ok {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
|
return
|
|
}
|
|
|
|
var req updateRoleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
role := strings.ToLower(strings.TrimSpace(req.Role))
|
|
if role != "admin" && role != "user" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "role must be admin or user"})
|
|
return
|
|
}
|
|
|
|
if err := db.DB.Model(&model.User{}).Where("id = ?", userID).Update("role", role).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update role"})
|
|
return
|
|
}
|
|
|
|
operator, _ := c.Get("username")
|
|
monitor.Auditf("admin=%v updated user_id=%d role=%s", operator, userID, role)
|
|
c.JSON(http.StatusOK, gin.H{"message": "role updated"})
|
|
}
|
|
|
|
type updateEnabledRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func UpdateUserEnabled(c *gin.Context) {
|
|
userID, ok := parsePathUint(c.Param("id"))
|
|
if !ok {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
|
return
|
|
}
|
|
|
|
var req updateEnabledRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := db.DB.Model(&model.User{}).Where("id = ?", userID).Update("enabled", req.Enabled).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update enabled status"})
|
|
return
|
|
}
|
|
|
|
operator, _ := c.Get("username")
|
|
monitor.Auditf("admin=%v updated user_id=%d enabled=%v", operator, userID, req.Enabled)
|
|
c.JSON(http.StatusOK, gin.H{"message": "enabled status updated"})
|
|
}
|
|
|
|
type resetPasswordRequest struct {
|
|
NewPassword string `json:"new_password" binding:"required"`
|
|
}
|
|
|
|
func ResetUserPassword(c *gin.Context) {
|
|
userID, ok := parsePathUint(c.Param("id"))
|
|
if !ok {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
|
return
|
|
}
|
|
|
|
var req resetPasswordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
hash, err := utils.HashPassword(req.NewPassword)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"})
|
|
return
|
|
}
|
|
|
|
if err := db.DB.Model(&model.User{}).Where("id = ?", userID).Update("password", hash).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reset password"})
|
|
return
|
|
}
|
|
|
|
operator, _ := c.Get("username")
|
|
monitor.Auditf("admin=%v reset password for user_id=%d", operator, userID)
|
|
c.JSON(http.StatusOK, gin.H{"message": "password reset"})
|
|
}
|
|
|
|
func DeleteUser(c *gin.Context) {
|
|
userID, ok := parsePathUint(c.Param("id"))
|
|
if !ok {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
|
return
|
|
}
|
|
|
|
operatorID, _ := c.Get("user_id")
|
|
if opID, ok := operatorID.(uint); ok && opID == userID {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot delete current admin account"})
|
|
return
|
|
}
|
|
|
|
if err := db.DB.Where("user_id = ?", userID).Delete(&model.Room{}).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete rooms"})
|
|
return
|
|
}
|
|
if err := db.DB.Delete(&model.User{}, userID).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete user"})
|
|
return
|
|
}
|
|
|
|
operator, _ := c.Get("username")
|
|
monitor.Auditf("admin=%v deleted user_id=%d", operator, userID)
|
|
c.JSON(http.StatusOK, gin.H{"message": "user deleted"})
|
|
}
|
|
|
|
func ListUsers(c *gin.Context) {
|
|
keyword := strings.TrimSpace(c.Query("keyword"))
|
|
page := parseIntWithDefault(c.Query("page"), 1)
|
|
pageSize := parseIntWithDefault(c.Query("page_size"), 20)
|
|
if pageSize > 200 {
|
|
pageSize = 200
|
|
}
|
|
offset := (page - 1) * pageSize
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
|
|
query := db.DB.Model(&model.User{})
|
|
if keyword != "" {
|
|
query = query.Where("username LIKE ?", "%"+keyword+"%")
|
|
}
|
|
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to count users"})
|
|
return
|
|
}
|
|
|
|
var users []model.User
|
|
if err := query.Order("id DESC").Offset(offset).Limit(pageSize).Find(&users).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to query users"})
|
|
return
|
|
}
|
|
|
|
items := make([]gin.H, 0, len(users))
|
|
for _, u := range users {
|
|
items = append(items, gin.H{
|
|
"id": u.ID,
|
|
"username": u.Username,
|
|
"role": u.Role,
|
|
"enabled": u.Enabled,
|
|
"created_at": u.CreatedAt,
|
|
"updated_at": u.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"items": items,
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": pageSize,
|
|
})
|
|
}
|
|
|
|
func parseIntWithDefault(v string, def int) int {
|
|
i, err := strconv.Atoi(v)
|
|
if err != nil || i <= 0 {
|
|
return def
|
|
}
|
|
return i
|
|
}
|
|
|
|
func parsePathUint(v string) (uint, bool) {
|
|
u64, err := strconv.ParseUint(v, 10, 32)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return uint(u64), true
|
|
}
|