Files
rmser/internal/transport/http/handlers/settings.go
SERTY b4ce819931 добавил пользователей для сервера и роли
добавил инвайт-ссылки с ролью оператор для сервера
добавил супер-админку для смены владельцев
добавил уведомления о смене ролей на серверах
добавил модалку для фото прям в черновике
добавил UI для редактирования прав
2025-12-23 13:06:06 +03:00

391 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handlers
import (
"net/http"
"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 // Поле для отправки уведомлений
}
func NewSettingsHandler(accRepo account.Repository, catRepo catalog.Repository) *SettingsHandler {
return &SettingsHandler{
accountRepo: accRepo,
catalogRepo: catRepo,
}
}
// 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
}
// 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),
}
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
type UpdateSettingsDTO struct {
Name string `json:"name"`
DefaultStoreID string `json:"default_store_id"`
RootGroupID string `json:"root_group_id"`
AutoProcess bool `json:"auto_process"`
AutoConduct bool `json:"auto_conduct"`
}
// UpdateSettings сохраняет настройки
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
}
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 != "" {
server.Name = req.Name
}
if req.AutoConduct {
server.AutoProcess = true
} else {
server.AutoProcess = req.AutoProcess || req.AutoConduct
}
if req.DefaultStoreID != "" {
if uid, err := uuid.Parse(req.DefaultStoreID); err == nil {
server.DefaultStoreID = &uid
}
} else {
server.DefaultStoreID = nil
}
if req.RootGroupID != "" {
if uid, err := uuid.Parse(req.RootGroupID); err == nil {
server.RootGroupGUID = &uid
}
} else {
server.RootGroupGUID = 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"})
}