добавил пользователей для сервера и роли

добавил инвайт-ссылки с ролью оператор для сервера
добавил супер-админку для смены владельцев
добавил уведомления о смене ролей на серверах
добавил модалку для фото прям в черновике
добавил UI для редактирования прав
This commit is contained in:
2025-12-23 13:06:06 +03:00
parent 9441579a34
commit b4ce819931
21 changed files with 9244 additions and 418 deletions

View File

@@ -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"})
}