mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
добавил пользователей для сервера и роли
добавил инвайт-ссылки с ролью оператор для сервера добавил супер-админку для смены владельцев добавил уведомления о смене ролей на серверах добавил модалку для фото прям в черновике добавил UI для редактирования прав
This commit is contained in:
@@ -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(¤tCount)
|
||||
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(¤tOwnerLink).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(¤tOwnerLink).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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user