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

добавил инвайт-ссылки с ролью оператор для сервера
добавил супер-админку для смены владельцев
добавил уведомления о смене ролей на серверах
добавил модалку для фото прям в черновике
добавил 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

@@ -1,11 +1,14 @@
package account
import (
"errors"
"fmt"
"strings"
"rmser/internal/domain/account"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type pgRepository struct {
@@ -23,7 +26,6 @@ func (r *pgRepository) GetOrCreateUser(telegramID int64, username, first, last s
if err != nil {
if err == gorm.ErrRecordNotFound {
// Создаем
newUser := account.User{
TelegramID: telegramID,
Username: username,
@@ -38,8 +40,8 @@ func (r *pgRepository) GetOrCreateUser(telegramID int64, username, first, last s
return nil, err
}
// Обновляем инфо, если изменилось (опционально)
if user.Username != username || user.FirstName != first {
// Обновляем инфо
if user.Username != username || user.FirstName != first || user.LastName != last {
user.Username = username
user.FirstName = first
user.LastName = last
@@ -51,65 +53,298 @@ func (r *pgRepository) GetOrCreateUser(telegramID int64, username, first, last s
func (r *pgRepository) GetUserByTelegramID(telegramID int64) (*account.User, error) {
var user account.User
// Preload Servers чтобы сразу видеть подключения
err := r.db.Preload("Servers").Where("telegram_id = ?", telegramID).First(&user).Error
err := r.db.Where("telegram_id = ?", telegramID).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *pgRepository) SaveServer(server *account.RMSServer) error {
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(server).Error
// ConnectServer - Основная точка входа для добавления сервера
func (r *pgRepository) ConnectServer(userID uuid.UUID, rawURL, login, encryptedPass, name string) (*account.RMSServer, error) {
// 1. Нормализация URL (удаляем слеш в конце, приводим к нижнему регистру)
// Важно: мы не удаляем http/https, так как iiko может работать и так и так, но обычно это разные эндпоинты.
// Для надежности уникальности можно вырезать протокол, но пока оставим как есть, просто тримминг.
cleanURL := strings.TrimRight(strings.ToLower(strings.TrimSpace(rawURL)), "/")
var server account.RMSServer
var created bool
err := r.db.Transaction(func(tx *gorm.DB) error {
// 2. Ищем, существует ли сервер с таким URL
err := tx.Where("base_url = ?", cleanURL).First(&server).Error
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
if err == gorm.ErrRecordNotFound {
// --- СЦЕНАРИЙ 1: НОВЫЙ СЕРВЕР ---
server = account.RMSServer{
BaseURL: cleanURL,
Name: name,
MaxUsers: 5, // Дефолтное ограничение
}
if err := tx.Create(&server).Error; err != nil {
return err
}
created = true
} else {
// --- СЦЕНАРИЙ 2: СУЩЕСТВУЮЩИЙ СЕРВЕР ---
// Проверяем лимит пользователей
var userCount int64
tx.Model(&account.ServerUser{}).Where("server_id = ?", server.ID).Count(&userCount)
if userCount >= int64(server.MaxUsers) {
// Проверяем, может пользователь УЖЕ там есть? Тогда это не добавление, а обновление кредов
var exists int64
tx.Model(&account.ServerUser{}).Where("server_id = ? AND user_id = ?", server.ID, userID).Count(&exists)
if exists == 0 {
return fmt.Errorf("достигнут лимит пользователей на сервере (%d)", server.MaxUsers)
}
}
}
// 3. Определяем роль
targetRole := account.RoleOperator
if created {
targetRole = account.RoleOwner
}
// 4. Создаем или обновляем связь с пользователем
// Сбрасываем активность других серверов
if err := tx.Model(&account.ServerUser{}).Where("user_id = ?", userID).Update("is_active", false).Error; err != nil {
return err
}
userLink := account.ServerUser{
ServerID: server.ID,
UserID: userID,
Role: targetRole,
IsActive: true,
Login: login,
EncryptedPassword: encryptedPass,
}
// Upsert для связи (на случай если пользователь подключает уже подключенный сервер, обновляем пароль)
// Если пользователь уже был OWNER/ADMIN, роль НЕ понижаем.
// Поэтому используем хитрый Upsert: обновляем роль только если запись новая.
var existingLink account.ServerUser
err = tx.Where("server_id = ? AND user_id = ?", server.ID, userID).First(&existingLink).Error
if err == nil {
// Запись есть -> обновляем креды и делаем активным, роль НЕ меняем
existingLink.Login = login
existingLink.EncryptedPassword = encryptedPass
existingLink.IsActive = true
return tx.Save(&existingLink).Error
}
// Записи нет -> создаем с вычисленной ролью
return tx.Create(&userLink).Error
})
if err != nil {
return nil, err
}
return &server, nil
}
func (r *pgRepository) SaveServerSettings(server *account.RMSServer) error {
return r.db.Model(server).Updates(map[string]interface{}{
"name": server.Name,
"default_store_id": server.DefaultStoreID,
"root_group_guid": server.RootGroupGUID,
"auto_process": server.AutoProcess,
"max_users": server.MaxUsers,
}).Error
}
// SetActiveServer делает указанный сервер активным, а остальные — неактивными
func (r *pgRepository) SetActiveServer(userID, serverID uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 1. Сбрасываем флаг у всех серверов пользователя
if err := tx.Model(&account.RMSServer{}).
Where("user_id = ?", userID).
Update("is_active", false).Error; err != nil {
return err
// Проверка доступа
var count int64
tx.Model(&account.ServerUser{}).Where("user_id = ? AND server_id = ?", userID, serverID).Count(&count)
if count == 0 {
return errors.New("доступ к серверу запрещен")
}
// 2. Ставим флаг целевому серверу
if err := tx.Model(&account.RMSServer{}).
Where("id = ? AND user_id = ?", serverID, userID).
Update("is_active", true).Error; err != nil {
if err := tx.Model(&account.ServerUser{}).Where("user_id = ?", userID).Update("is_active", false).Error; err != nil {
return err
}
return nil
return tx.Model(&account.ServerUser{}).Where("user_id = ? AND server_id = ?", userID, serverID).Update("is_active", true).Error
})
}
func (r *pgRepository) GetActiveServer(userID uuid.UUID) (*account.RMSServer, error) {
var server account.RMSServer
// Берем первый активный сервер. В будущем можно добавить поле IsSelected
err := r.db.Where("user_id = ? AND is_active = ?", userID, true).First(&server).Error
err := r.db.Table("rms_servers").
Select("rms_servers.*").
Joins("JOIN server_users ON server_users.server_id = rms_servers.id").
Where("server_users.user_id = ? AND server_users.is_active = ?", userID, true).
First(&server).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil // Нет серверов
return nil, nil
}
return nil, err
}
return &server, nil
}
// GetAllServers возвращает ВСЕ серверы пользователя, чтобы можно было переключаться
func (r *pgRepository) GetAllServers(userID uuid.UUID) ([]account.RMSServer, error) {
// GetActiveConnectionCredentials возвращает креды для подключения.
// Логика:
// 1. Берем личные креды из server_users (если активен)
// 2. Если личных нет (пустой пароль) -> ищем креды Владельца (Owner) этого сервера
func (r *pgRepository) GetActiveConnectionCredentials(userID uuid.UUID) (url, login, passHash string, err error) {
// 1. Получаем связь текущего юзера с активным сервером
type Result struct {
ServerID uuid.UUID
BaseURL string
Login string
EncryptedPassword string
}
var res Result
err = r.db.Table("server_users").
Select("server_users.server_id, rms_servers.base_url, server_users.login, server_users.encrypted_password").
Joins("JOIN rms_servers ON rms_servers.id = server_users.server_id").
Where("server_users.user_id = ? AND server_users.is_active = ?", userID, true).
Scan(&res).Error
if err != nil {
return "", "", "", err
}
if res.ServerID == uuid.Nil {
return "", "", "", errors.New("нет активного сервера")
}
// Если есть личные креды - возвращаем их
if res.Login != "" && res.EncryptedPassword != "" {
return res.BaseURL, res.Login, res.EncryptedPassword, nil
}
// 2. Фоллбэк: ищем креды владельца (OWNER)
var ownerLink account.ServerUser
err = r.db.Where("server_id = ? AND role = ?", res.ServerID, account.RoleOwner).
Order("created_at ASC"). // На случай коллизий, берем старейшего
First(&ownerLink).Error
if err != nil {
return "", "", "", fmt.Errorf("у вас нет учетных данных, а владелец сервера не найден: %w", err)
}
if ownerLink.Login == "" || ownerLink.EncryptedPassword == "" {
return "", "", "", errors.New("у владельца сервера отсутствуют учетные данные")
}
return res.BaseURL, ownerLink.Login, ownerLink.EncryptedPassword, nil
}
func (r *pgRepository) GetAllAvailableServers(userID uuid.UUID) ([]account.RMSServer, error) {
var servers []account.RMSServer
// Убрали фильтр AND is_active = true, теперь возвращает весь список
err := r.db.Where("user_id = ?", userID).Find(&servers).Error
err := r.db.Table("rms_servers").
Select("rms_servers.*").
Joins("JOIN server_users ON server_users.server_id = rms_servers.id").
Where("server_users.user_id = ?", userID).
Find(&servers).Error
return servers, err
}
func (r *pgRepository) DeleteServer(serverID uuid.UUID) error {
return r.db.Delete(&account.RMSServer{}, serverID).Error
// Полное удаление сервера и всех связей
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("server_id = ?", serverID).Delete(&account.ServerUser{}).Error; err != nil {
return err
}
if err := tx.Delete(&account.RMSServer{}, serverID).Error; err != nil {
return err
}
return nil
})
}
// --- Управление правами ---
func (r *pgRepository) GetUserRole(userID, serverID uuid.UUID) (account.Role, error) {
var link account.ServerUser
err := r.db.Select("role").Where("user_id = ? AND server_id = ?", userID, serverID).First(&link).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return "", errors.New("access denied")
}
return "", err
}
return link.Role, nil
}
func (r *pgRepository) SetUserRole(serverID, targetUserID uuid.UUID, newRole account.Role) error {
return r.db.Model(&account.ServerUser{}).
Where("server_id = ? AND user_id = ?", serverID, targetUserID).
Update("role", newRole).Error
}
func (r *pgRepository) GetServerUsers(serverID uuid.UUID) ([]account.ServerUser, error) {
var users []account.ServerUser
// Preload User для отображения имен
err := r.db.Preload("User").Where("server_id = ?", serverID).Find(&users).Error
return users, err
}
func (r *pgRepository) AddUserToServer(serverID, userID uuid.UUID, role account.Role) error {
// Проверка лимита перед добавлением
var server account.RMSServer
if err := r.db.First(&server, serverID).Error; err != nil {
return err
}
return r.db.Transaction(func(tx *gorm.DB) error {
// 1. Сначала проверяем, существует ли пользователь на этом сервере
var existingLink account.ServerUser
err := tx.Where("server_id = ? AND user_id = ?", serverID, userID).First(&existingLink).Error
if err == nil {
// --- ПОЛЬЗОВАТЕЛЬ УЖЕ ЕСТЬ ---
// Защита от понижения прав:
// Если текущая роль OWNER или ADMIN, а мы пытаемся поставить OPERATOR (через инвайт),
// то игнорируем смену роли, просто делаем активным.
if (existingLink.Role == account.RoleOwner || existingLink.Role == account.RoleAdmin) && role == account.RoleOperator {
role = existingLink.Role
}
// Обновляем активность и (возможно) роль
return tx.Model(&existingLink).Updates(map[string]interface{}{
"role": role,
"is_active": true,
}).Error
}
// --- ПОЛЬЗОВАТЕЛЬ НОВЫЙ ---
// Проверяем лимит только для новых
var currentCount int64
tx.Model(&account.ServerUser{}).Where("server_id = ?", serverID).Count(&currentCount)
if currentCount >= int64(server.MaxUsers) {
return fmt.Errorf("лимит пользователей (%d) превышен", server.MaxUsers)
}
// Сбрасываем активность на других серверах
if err := tx.Model(&account.ServerUser{}).Where("user_id = ?", userID).Update("is_active", false).Error; err != nil {
return err
}
// Создаем связь
link := account.ServerUser{
ServerID: serverID,
UserID: userID,
Role: role,
IsActive: true,
}
return tx.Create(&link).Error
})
}
func (r *pgRepository) RemoveUserFromServer(serverID, userID uuid.UUID) error {
return r.db.Where("server_id = ? AND user_id = ?", serverID, userID).Delete(&account.ServerUser{}).Error
}
func (r *pgRepository) IncrementInvoiceCount(serverID uuid.UUID) error {
@@ -117,3 +352,52 @@ func (r *pgRepository) IncrementInvoiceCount(serverID uuid.UUID) error {
Where("id = ?", serverID).
UpdateColumn("invoice_count", gorm.Expr("invoice_count + ?", 1)).Error
}
// --- Super Admin Functions ---
func (r *pgRepository) GetAllServersSystemWide() ([]account.RMSServer, error) {
var servers []account.RMSServer
// Загружаем вместе с владельцем для отображения
err := r.db.Order("name ASC").Find(&servers).Error
return servers, err
}
func (r *pgRepository) TransferOwnership(serverID, newOwnerID uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 1. Находим текущего владельца
var currentOwnerLink account.ServerUser
if err := tx.Where("server_id = ? AND role = ?", serverID, account.RoleOwner).First(&currentOwnerLink).Error; err != nil {
return fmt.Errorf("current owner not found: %w", err)
}
// 2. Проверяем, что новый владелец вообще есть на сервере
var newOwnerLink account.ServerUser
if err := tx.Where("server_id = ? AND user_id = ?", serverID, newOwnerID).First(&newOwnerLink).Error; err != nil {
return fmt.Errorf("target user not found on server: %w", err)
}
// 3. Понижаем старого владельца до ADMIN
if err := tx.Model(&currentOwnerLink).Update("role", account.RoleAdmin).Error; err != nil {
return err
}
// 4. Повышаем нового до OWNER
if err := tx.Model(&newOwnerLink).Update("role", account.RoleOwner).Error; err != nil {
return err
}
// УДАЛЕНО: обновление server.owner_id, так как этого поля нет в модели
return nil
})
}
func (r *pgRepository) GetConnectionByID(id uuid.UUID) (*account.ServerUser, error) {
var link account.ServerUser
// Preload нужны, чтобы показать имена в админке
err := r.db.Preload("User").Preload("Server").Where("id = ?", id).First(&link).Error
if err != nil {
return nil, err
}
return &link, nil
}

View File

@@ -39,7 +39,6 @@ func (r *pgRepository) GetByID(id uuid.UUID) (*drafts.DraftInvoice, error) {
}
func (r *pgRepository) Update(draft *drafts.DraftInvoice) error {
// Обновляем поля шапки + привязки к серверу
return r.db.Model(draft).Updates(map[string]interface{}{
"status": draft.Status,
"document_number": draft.DocumentNumber,
@@ -48,7 +47,7 @@ func (r *pgRepository) Update(draft *drafts.DraftInvoice) error {
"store_id": draft.StoreID,
"comment": draft.Comment,
"rms_invoice_id": draft.RMSInvoiceID,
"rms_server_id": draft.RMSServerID, // Вдруг поменялся, хотя не должен
"rms_server_id": draft.RMSServerID,
"updated_at": gorm.Expr("NOW()"),
}).Error
}
@@ -88,8 +87,8 @@ func (r *pgRepository) Delete(id uuid.UUID) error {
return r.db.Delete(&drafts.DraftInvoice{}, id).Error
}
// GetActive фильтрует по UserID
func (r *pgRepository) GetActive(userID uuid.UUID) ([]drafts.DraftInvoice, error) {
// GetActive возвращает черновики для конкретного СЕРВЕРА
func (r *pgRepository) GetActive(serverID uuid.UUID) ([]drafts.DraftInvoice, error) {
var list []drafts.DraftInvoice
activeStatuses := []string{
@@ -102,7 +101,7 @@ func (r *pgRepository) GetActive(userID uuid.UUID) ([]drafts.DraftInvoice, error
err := r.db.
Preload("Items").
Preload("Store").
Where("user_id = ? AND status IN ?", userID, activeStatuses). // <-- FILTER
Where("rms_server_id = ? AND status IN ?", serverID, activeStatuses). // Фильтр по серверу
Order("created_at DESC").
Find(&list).Error