mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
добавил пользователей для сервера и роли
добавил инвайт-ссылки с ролью оператор для сервера добавил супер-админку для смены владельцев добавил уведомления о смене ролей на серверах добавил модалку для фото прям в черновике добавил UI для редактирования прав
This commit is contained in:
@@ -12,9 +12,16 @@ import (
|
||||
"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 {
|
||||
@@ -24,7 +31,23 @@ func NewSettingsHandler(accRepo account.Repository, catRepo catalog.Repository)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSettings возвращает настройки активного сервера
|
||||
// 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)
|
||||
|
||||
@@ -38,7 +61,29 @@ func (h *SettingsHandler) GetSettings(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, server)
|
||||
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
|
||||
@@ -47,6 +92,7 @@ type UpdateSettingsDTO struct {
|
||||
DefaultStoreID string `json:"default_store_id"`
|
||||
RootGroupID string `json:"root_group_id"`
|
||||
AutoProcess bool `json:"auto_process"`
|
||||
AutoConduct bool `json:"auto_conduct"`
|
||||
}
|
||||
|
||||
// UpdateSettings сохраняет настройки
|
||||
@@ -65,11 +111,26 @@ func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
server.AutoProcess = req.AutoProcess
|
||||
|
||||
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 {
|
||||
@@ -79,7 +140,6 @@ func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
|
||||
server.DefaultStoreID = nil
|
||||
}
|
||||
|
||||
// Теперь правильно ловим ID группы
|
||||
if req.RootGroupID != "" {
|
||||
if uid, err := uuid.Parse(req.RootGroupID); err == nil {
|
||||
server.RootGroupGUID = &uid
|
||||
@@ -88,25 +148,24 @@ func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
|
||||
server.RootGroupGUID = nil
|
||||
}
|
||||
|
||||
if err := h.accountRepo.SaveServer(server); err != 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, server)
|
||||
h.GetSettings(c)
|
||||
}
|
||||
|
||||
// --- Group Tree Logic ---
|
||||
|
||||
type GroupNode struct {
|
||||
Key string `json:"key"` // ID for Ant Design TreeSelect
|
||||
Value string `json:"value"` // ID value
|
||||
Title string `json:"title"` // Name
|
||||
Children []*GroupNode `json:"children"` // Sub-groups
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Title string `json:"title"`
|
||||
Children []*GroupNode `json:"children"`
|
||||
}
|
||||
|
||||
// GetGroupsTree возвращает иерархию групп
|
||||
func (h *SettingsHandler) GetGroupsTree(c *gin.Context) {
|
||||
userID := c.MustGet("userID").(uuid.UUID)
|
||||
server, err := h.accountRepo.GetActiveServer(userID)
|
||||
@@ -126,7 +185,6 @@ func (h *SettingsHandler) GetGroupsTree(c *gin.Context) {
|
||||
}
|
||||
|
||||
func buildTree(flat []catalog.Product) []*GroupNode {
|
||||
// 1. Map ID -> Node
|
||||
nodeMap := make(map[uuid.UUID]*GroupNode)
|
||||
for _, g := range flat {
|
||||
nodeMap[g.ID] = &GroupNode{
|
||||
@@ -138,16 +196,12 @@ func buildTree(flat []catalog.Product) []*GroupNode {
|
||||
}
|
||||
|
||||
var roots []*GroupNode
|
||||
|
||||
// 2. Build Hierarchy
|
||||
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 {
|
||||
@@ -156,3 +210,181 @@ func buildTree(flat []catalog.Product) []*GroupNode {
|
||||
}
|
||||
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"})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user