package handlers import ( "net/http" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" "rmser/internal/domain/account" "rmser/internal/domain/catalog" "rmser/pkg/logger" ) // Notifier - интерфейс для отправки уведомлений (реализуется ботом) type Notifier interface { SendRoleChangeNotification(telegramID int64, serverName string, newRole string) SendRemovalNotification(telegramID int64, serverName string) } type SettingsHandler struct { accountRepo account.Repository catalogRepo catalog.Repository notifier Notifier // Поле для отправки уведомлений rmsFactory RMSFactory } type RMSFactory interface { ClearCacheForUser(userID uuid.UUID) } func NewSettingsHandler(accRepo account.Repository, catRepo catalog.Repository) *SettingsHandler { return &SettingsHandler{ accountRepo: accRepo, catalogRepo: catRepo, } } // SetRMSFactory используется для внедрения зависимости после инициализации func (h *SettingsHandler) SetRMSFactory(f RMSFactory) { h.rmsFactory = f } // SetNotifier используется для внедрения зависимости после инициализации func (h *SettingsHandler) SetNotifier(n Notifier) { h.notifier = n } // SettingsResponse - DTO для отдачи настроек type SettingsResponse struct { ID string `json:"id"` Name string `json:"name"` BaseURL string `json:"base_url"` DefaultStoreID *string `json:"default_store_id"` // Nullable RootGroupID *string `json:"root_group_id"` // Nullable AutoConduct bool `json:"auto_conduct"` Role string `json:"role"` // OWNER, ADMIN, OPERATOR SyncInterval int `json:"sync_interval"` // Интервал синхронизации в минутах LastSyncAt *time.Time `json:"last_sync_at"` // Время последней синхронизации LastActivityAt *time.Time `json:"last_activity_at"` // Время последней активности } // GetSettings возвращает настройки активного сервера + роль пользователя func (h *SettingsHandler) GetSettings(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) server, err := h.accountRepo.GetActiveServer(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if server == nil { c.JSON(http.StatusNotFound, gin.H{"error": "No active server"}) return } role, err := h.accountRepo.GetUserRole(userID, server.ID) if err != nil { role = account.RoleOperator } resp := SettingsResponse{ ID: server.ID.String(), Name: server.Name, BaseURL: server.BaseURL, AutoConduct: server.AutoProcess, Role: string(role), SyncInterval: server.SyncInterval, LastSyncAt: server.LastSyncAt, LastActivityAt: server.LastActivityAt, } if server.DefaultStoreID != nil { s := server.DefaultStoreID.String() resp.DefaultStoreID = &s } if server.RootGroupGUID != nil { s := server.RootGroupGUID.String() resp.RootGroupID = &s } c.JSON(http.StatusOK, resp) } // UpdateSettingsDTO - DTO для частичного обновления настроек (PATCH-семантика) type UpdateSettingsDTO struct { Name *string `json:"name"` DefaultStoreID *string `json:"default_store_id"` RootGroupID *string `json:"root_group_id"` AutoProcess *bool `json:"auto_process"` // Legacy для обратной совместимости AutoConduct *bool `json:"auto_conduct"` // Новое поле SyncInterval *int `json:"sync_interval,omitempty"` // Интервал синхронизации в минутах (5 - 10080) } // UpdateSettings сохраняет настройки с PATCH-семантикой func (h *SettingsHandler) UpdateSettings(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) var req UpdateSettingsDTO if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Логирование полученных данных для отладки logger.Log.Info("Получен запрос на обновление настроек", zap.Any("request", req), ) server, err := h.accountRepo.GetActiveServer(userID) if err != nil || server == nil { c.JSON(http.StatusNotFound, gin.H{"error": "No active server"}) return } // ПРОВЕРКА ПРАВ role, err := h.accountRepo.GetUserRole(userID, server.ID) if err != nil { c.JSON(http.StatusForbidden, gin.H{"error": "Access check failed"}) return } if role != account.RoleOwner && role != account.RoleAdmin { c.JSON(http.StatusForbidden, gin.H{"error": "У вас нет прав на изменение настроек сервера (требуется ADMIN или OWNER)"}) return } // Обновление имени (только если передано) if req.Name != nil { server.Name = *req.Name } // Обновление флага авто-проведения if req.AutoConduct != nil { server.AutoProcess = *req.AutoConduct } else if req.AutoProcess != nil { // Fallback для старых клиентов, которые используют legacy поле server.AutoProcess = *req.AutoProcess } // Обновление интервала синхронизации if req.SyncInterval != nil { // Валидация диапазона: от 5 минут до 1 недели (10080 минут) if *req.SyncInterval < 5 || *req.SyncInterval > 10080 { c.JSON(http.StatusBadRequest, gin.H{"error": "sync_interval должен быть от 5 минут до 1 недели (10080 минут)"}) return } server.SyncInterval = *req.SyncInterval } // Обновление DefaultStoreID if req.DefaultStoreID != nil { if *req.DefaultStoreID == "" { // Пустая строка -> сбрасываем в nil server.DefaultStoreID = nil } else { // UUID -> обновляем if uid, err := uuid.Parse(*req.DefaultStoreID); err == nil { server.DefaultStoreID = &uid } } } // Если nil -> не трогаем текущее значение // Обновление RootGroupID if req.RootGroupID != nil { if *req.RootGroupID == "" { // Пустая строка -> сбрасываем в nil server.RootGroupGUID = nil } else { // UUID -> обновляем if uid, err := uuid.Parse(*req.RootGroupID); err == nil { server.RootGroupGUID = &uid } } } // Если nil -> не трогаем текущее значение if err := h.accountRepo.SaveServerSettings(server); err != nil { logger.Log.Error("Failed to save settings", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } h.GetSettings(c) } // --- Group Tree Logic --- type GroupNode struct { Key string `json:"key"` Value string `json:"value"` Title string `json:"title"` Children []*GroupNode `json:"children"` } func (h *SettingsHandler) GetGroupsTree(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) server, err := h.accountRepo.GetActiveServer(userID) if err != nil || server == nil { c.JSON(http.StatusNotFound, gin.H{"error": "No active server"}) return } groups, err := h.catalogRepo.GetGroups(server.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } tree := buildTree(groups) c.JSON(http.StatusOK, tree) } func buildTree(flat []catalog.Product) []*GroupNode { nodeMap := make(map[uuid.UUID]*GroupNode) for _, g := range flat { nodeMap[g.ID] = &GroupNode{ Key: g.ID.String(), Value: g.ID.String(), Title: g.Name, Children: make([]*GroupNode, 0), } } var roots []*GroupNode for _, g := range flat { node := nodeMap[g.ID] if g.ParentID != nil { if parent, exists := nodeMap[*g.ParentID]; exists { parent.Children = append(parent.Children, node) } else { roots = append(roots, node) } } else { roots = append(roots, node) } } return roots } // --- User Management --- func (h *SettingsHandler) GetServerUsers(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) server, err := h.accountRepo.GetActiveServer(userID) if err != nil || server == nil { c.JSON(http.StatusNotFound, gin.H{"error": "No active server"}) return } myRole, err := h.accountRepo.GetUserRole(userID, server.ID) if err != nil || (myRole != account.RoleOwner && myRole != account.RoleAdmin) { c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) return } users, err := h.accountRepo.GetServerUsers(server.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } type UserDTO struct { UserID uuid.UUID `json:"user_id"` Username string `json:"username"` FirstName string `json:"first_name"` LastName string `json:"last_name"` PhotoURL string `json:"photo_url"` Role account.Role `json:"role"` IsMe bool `json:"is_me"` } response := make([]UserDTO, 0, len(users)) for _, u := range users { response = append(response, UserDTO{ UserID: u.UserID, Username: u.User.Username, FirstName: u.User.FirstName, LastName: u.User.LastName, PhotoURL: u.User.PhotoURL, Role: u.Role, IsMe: u.UserID == userID, }) } c.JSON(http.StatusOK, response) } type UpdateUserRoleDTO struct { NewRole string `json:"new_role" binding:"required"` // ADMIN, OPERATOR } func (h *SettingsHandler) UpdateUserRole(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) targetUserID, err := uuid.Parse(c.Param("userId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target user id"}) return } var req UpdateUserRoleDTO if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } newRole := account.Role(req.NewRole) if newRole != account.RoleAdmin && newRole != account.RoleOperator { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid role (allowed: ADMIN, OPERATOR)"}) return } server, err := h.accountRepo.GetActiveServer(userID) if err != nil || server == nil { c.JSON(http.StatusNotFound, gin.H{"error": "No active server"}) return } myRole, _ := h.accountRepo.GetUserRole(userID, server.ID) if myRole != account.RoleOwner { c.JSON(http.StatusForbidden, gin.H{"error": "Only OWNER can change roles"}) return } if userID == targetUserID { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change own role"}) return } if err := h.accountRepo.SetUserRole(server.ID, targetUserID, newRole); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // --- УВЕДОМЛЕНИЕ О СМЕНЕ РОЛИ --- if h.notifier != nil { go func() { users, err := h.accountRepo.GetServerUsers(server.ID) if err == nil { for _, u := range users { if u.UserID == targetUserID { h.notifier.SendRoleChangeNotification(u.User.TelegramID, server.Name, string(newRole)) break } } } }() } c.JSON(http.StatusOK, gin.H{"status": "updated"}) } func (h *SettingsHandler) RemoveUser(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) targetUserID, err := uuid.Parse(c.Param("userId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target user id"}) return } server, err := h.accountRepo.GetActiveServer(userID) if err != nil || server == nil { c.JSON(http.StatusNotFound, gin.H{"error": "No active server"}) return } myRole, _ := h.accountRepo.GetUserRole(userID, server.ID) if myRole != account.RoleOwner && myRole != account.RoleAdmin { c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) return } if userID == targetUserID { c.JSON(http.StatusBadRequest, gin.H{"error": "Use 'leave' function instead"}) return } // Ищем цель в списке, чтобы проверить права и получить TelegramID для уведомления users, _ := h.accountRepo.GetServerUsers(server.ID) var targetTgID int64 var found bool for _, u := range users { if u.UserID == targetUserID { found = true targetTgID = u.User.TelegramID if u.Role == account.RoleOwner { c.JSON(http.StatusForbidden, gin.H{"error": "Cannot remove Owner"}) return } if myRole == account.RoleAdmin && u.Role == account.RoleAdmin { c.JSON(http.StatusForbidden, gin.H{"error": "Admins cannot remove other Admins"}) return } break } } if !found { c.JSON(http.StatusNotFound, gin.H{"error": "Target user not found on server"}) return } if err := h.accountRepo.RemoveUserFromServer(server.ID, targetUserID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // --- УВЕДОМЛЕНИЕ ОБ УДАЛЕНИИ --- if h.notifier != nil && targetTgID != 0 { go h.notifier.SendRemovalNotification(targetTgID, server.Name) } c.JSON(http.StatusOK, gin.H{"status": "removed"}) } // --- Server Management --- // ServerShortDTO - краткая информация о сервере для списка type ServerShortDTO struct { ID string `json:"id"` Name string `json:"name"` Role string `json:"role"` // OWNER, ADMIN, OPERATOR IsActive bool `json:"is_active"` } // GetUserServers возвращает список всех серверов пользователя с ролями и флагом активности func (h *SettingsHandler) GetUserServers(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) servers, err := h.accountRepo.GetAllAvailableServers(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } activeServer, err := h.accountRepo.GetActiveServer(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } var activeServerID *uuid.UUID if activeServer != nil { activeServerID = &activeServer.ID } response := make([]ServerShortDTO, 0, len(servers)) for _, server := range servers { role, err := h.accountRepo.GetUserRole(userID, server.ID) if err != nil { role = account.RoleOperator } response = append(response, ServerShortDTO{ ID: server.ID.String(), Name: server.Name, Role: string(role), IsActive: activeServerID != nil && server.ID == *activeServerID, }) } c.JSON(http.StatusOK, response) } // SwitchActiveServerRequest - запрос на переключение активного сервера type SwitchActiveServerRequest struct { ServerID string `json:"server_id" binding:"required"` } // SwitchActiveServer переключает активный сервер пользователя func (h *SettingsHandler) SwitchActiveServer(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) var req SwitchActiveServerRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } serverID, err := uuid.Parse(req.ServerID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid server_id format"}) return } // Проверяем, что сервер доступен пользователю servers, err := h.accountRepo.GetAllAvailableServers(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } var serverExists bool for _, s := range servers { if s.ID == serverID { serverExists = true break } } if !serverExists { c.JSON(http.StatusNotFound, gin.H{"error": "server not found or not accessible"}) return } // Переключаем активный сервер if err := h.accountRepo.SetActiveServer(userID, serverID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Сбрасываем кэш RMS клиента if h.rmsFactory != nil { h.rmsFactory.ClearCacheForUser(userID) } c.JSON(http.StatusOK, gin.H{"status": "active_server_changed"}) }