mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-05 03:12:34 -06:00
пересчет поправил редактирование с перепроведением галка автопроведения работает рекомендации починил
543 lines
16 KiB
Go
543 lines
16 KiB
Go
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"})
|
||
}
|