Files
rmser/internal/infrastructure/repository/account/postgres.go
SERTY ea1e5bbf6a 0302-добавил куки и сломал десктоп авторизацию.
сложно поддерживать однояйцевых близнецов - desktop и TMA, подготовил к рефакторингу структуры
2026-02-03 09:32:02 +03:00

584 lines
20 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 account
import (
"errors"
"fmt"
"strings"
"time"
"rmser/internal/domain/account"
"rmser/pkg/logger"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
)
type pgRepository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) account.Repository {
return &pgRepository{db: db}
}
// GetOrCreateUser находит пользователя или создает нового
func (r *pgRepository) GetOrCreateUser(telegramID int64, username, first, last string) (*account.User, error) {
var user account.User
err := r.db.Where("telegram_id = ?", telegramID).First(&user).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
newUser := account.User{
TelegramID: telegramID,
Username: username,
FirstName: first,
LastName: last,
}
if err := r.db.Create(&newUser).Error; err != nil {
return nil, err
}
return &newUser, nil
}
return nil, err
}
// Обновляем инфо
if user.Username != username || user.FirstName != first || user.LastName != last {
user.Username = username
user.FirstName = first
user.LastName = last
r.db.Save(&user)
}
return &user, nil
}
func (r *pgRepository) GetUserByTelegramID(telegramID int64) (*account.User, error) {
var user account.User
err := r.db.Where("telegram_id = ?", telegramID).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *pgRepository) GetUserByID(id uuid.UUID) (*account.User, error) {
var user account.User
err := r.db.Where("id = ?", id).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// ConnectServer - Основная точка входа для добавления сервера
func (r *pgRepository) ConnectServer(userID uuid.UUID, rawURL, login, encryptedPass, name string) (*account.RMSServer, error) {
cleanURL := strings.TrimRight(strings.ToLower(strings.TrimSpace(rawURL)), "/")
var server account.RMSServer
var created bool
err := r.db.Transaction(func(tx *gorm.DB) error {
// Сначала ищем среди удаленных серверов
err := tx.Unscoped().Where("base_url = ?", cleanURL).First(&server).Error
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
if err == gorm.ErrRecordNotFound {
// --- СЦЕНАРИЙ 1: НОВЫЙ СЕРВЕР (Приветственный бонус) ---
trialDays := 30
welcomeBalance := 10
paidUntil := time.Now().AddDate(0, 0, trialDays)
server = account.RMSServer{
BaseURL: cleanURL,
Name: name,
MaxUsers: 5,
Balance: welcomeBalance,
PaidUntil: &paidUntil,
}
if err := tx.Create(&server).Error; err != nil {
return err
}
created = true
} else if server.DeletedAt.Valid {
// --- СЦЕНАРИЙ 2: ВОССТАНОВЛЕНИЕ УДАЛЕННОГО СЕРВЕРА ---
// Восстанавливаем сервер, сохраняя старые значения Balance, InvoiceCount и ID
server.Name = name
server.DeletedAt = gorm.DeletedAt{} // Сбрасываем deleted_at
if err := tx.Save(&server).Error; err != nil {
return err
}
created = true // При восстановлении пользователь становится владельцем
} else {
// --- СЦЕНАРИЙ 3: СУЩЕСТВУЮЩИЙ АКТИВНЫЙ СЕРВЕР ---
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)
}
}
}
targetRole := account.RoleOperator
if created {
targetRole = account.RoleOwner
}
if err := tx.Model(&account.ServerUser{}).Where("user_id = ?", userID).Update("is_active", false).Error; err != nil {
return err
}
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
}
userLink := account.ServerUser{
ServerID: server.ID,
UserID: userID,
Role: targetRole,
IsActive: true,
Login: login,
EncryptedPassword: encryptedPass,
}
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,
"sync_interval": server.SyncInterval,
}).Error
}
// UpdateLastActivity обновляет время последней активности пользователя
func (r *pgRepository) UpdateLastActivity(serverID uuid.UUID) error {
result := r.db.Model(&account.RMSServer{}).
Where("id = ?", serverID).
Update("last_activity_at", gorm.Expr("NOW()"))
if result.Error != nil {
logger.Log.Error("Failed to update last_activity_at",
zap.String("server_id", serverID.String()),
zap.Error(result.Error))
return result.Error
}
if result.RowsAffected == 0 {
logger.Log.Warn("UpdateLastActivity: server not found",
zap.String("server_id", serverID.String()))
return fmt.Errorf("сервер не найден")
}
return nil
}
// UpdateLastSync обновляет время последней успешной синхронизации
func (r *pgRepository) UpdateLastSync(serverID uuid.UUID) error {
result := r.db.Model(&account.RMSServer{}).
Where("id = ?", serverID).
Update("last_sync_at", gorm.Expr("NOW()"))
if result.Error != nil {
logger.Log.Error("Failed to update last_sync_at",
zap.String("server_id", serverID.String()),
zap.Error(result.Error))
return result.Error
}
if result.RowsAffected == 0 {
logger.Log.Warn("UpdateLastSync: server not found",
zap.String("server_id", serverID.String()))
return fmt.Errorf("сервер не найден")
}
return nil
}
// GetServersForSync возвращает серверы, готовые для синхронизации
func (r *pgRepository) GetServersForSync(idleThreshold time.Duration) ([]account.RMSServer, error) {
var servers []account.RMSServer
// Конвертируем duration в минуты для SQL
idleMinutes := int(idleThreshold.Minutes())
query := `
SELECT * FROM rms_servers
WHERE
deleted_at IS NULL
AND (
-- Случай 1: Настало время периодической синхронизации
(EXTRACT(EPOCH FROM (NOW() - COALESCE(last_sync_at, '1970-01-01'::timestamp))) / 60) >= sync_interval
OR
-- Случай 2: Прошло N мин с последней активности, и активность была ПОЗЖЕ синхронизации
(
last_activity_at > last_sync_at
AND (EXTRACT(EPOCH FROM (NOW() - last_activity_at)) / 60) >= ?
)
)
`
err := r.db.Raw(query, idleMinutes).Scan(&servers).Error
if err != nil {
logger.Log.Error("Failed to get servers for sync",
zap.Int("idle_threshold_minutes", idleMinutes),
zap.Error(err))
return nil, err
}
logger.Log.Info("Servers ready for sync",
zap.Int("count", len(servers)),
zap.Int("idle_threshold_minutes", idleMinutes))
return servers, nil
}
func (r *pgRepository) SetActiveServer(userID, serverID uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Проверка доступа
var count int64
tx.Model(&account.ServerUser{}).Where("user_id = ? AND server_id = ?", userID, serverID).Count(&count)
if count == 0 {
return errors.New("доступ к серверу запрещен")
}
if err := tx.Model(&account.ServerUser{}).Where("user_id = ?", userID).Update("is_active", false).Error; err != nil {
return err
}
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
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, err
}
return &server, nil
}
// 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
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.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 {
return r.db.Model(&account.RMSServer{}).
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
}
func (r *pgRepository) GetServerByURL(rawURL string) (*account.RMSServer, error) {
cleanURL := strings.TrimRight(strings.ToLower(strings.TrimSpace(rawURL)), "/")
var server account.RMSServer
err := r.db.Where("base_url = ?", cleanURL).First(&server).Error
if err != nil {
return nil, err
}
return &server, nil
}
func (r *pgRepository) GetServerByID(id uuid.UUID) (*account.RMSServer, error) {
var server account.RMSServer
err := r.db.First(&server, id).Error
if err != nil {
return nil, err
}
return &server, nil
}
// UpdateBalance начисляет пакет или продлевает подписку
func (r *pgRepository) UpdateBalance(serverID uuid.UUID, amountChange int, newPaidUntil *time.Time) error {
return r.db.Model(&account.RMSServer{}).Where("id = ?", serverID).Updates(map[string]interface{}{
"balance": gorm.Expr("balance + ?", amountChange),
"paid_until": newPaidUntil,
}).Error
}
// DecrementBalance списывает 1 единицу при отправке накладной
func (r *pgRepository) DecrementBalance(serverID uuid.UUID) error {
return r.db.Model(&account.RMSServer{}).
Where("id = ? AND balance > 0", serverID).
UpdateColumn("balance", gorm.Expr("balance - ?", 1)).Error
}
// === Уведомления о черновиках ===
// GetServerUsersForDraftNotification возвращает Admin/Owner пользователей,
// которым нужно отправить уведомление о новом черновике
func (r *pgRepository) GetServerUsersForDraftNotification(serverID uuid.UUID, excludeUserID uuid.UUID) ([]account.ServerUser, error) {
var users []account.ServerUser
err := r.db.Preload("User").
Where("server_id = ?", serverID).
Where("role IN ?", []account.Role{account.RoleOwner, account.RoleAdmin}).
Where("mute_draft_notifications = ?", false).
Where("user_id != ?", excludeUserID).
Find(&users).Error
return users, err
}
// SetMuteDraftNotifications включает/выключает уведомления для пользователя
func (r *pgRepository) SetMuteDraftNotifications(userID, serverID uuid.UUID, mute bool) error {
return r.db.Model(&account.ServerUser{}).
Where("user_id = ? AND server_id = ?", userID, serverID).
Update("mute_draft_notifications", mute).Error
}
// === Session Management ===
// CreateSession создает новую сессию пользователя
func (r *pgRepository) CreateSession(session *account.Session) error {
return r.db.Create(session).Error
}
// GetSessionByToken ищет сессию по токену
func (r *pgRepository) GetSessionByToken(token string) (*account.Session, error) {
var session account.Session
if err := r.db.Where("refresh_token = ?", token).First(&session).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &session, nil
}
// UpdateSessionExpiry обновляет время истечения сессии (sliding expiration)
func (r *pgRepository) UpdateSessionExpiry(sessionID uuid.UUID, newExpiry time.Time) error {
return r.db.Model(&account.Session{}).Where("id = ?", sessionID).Update("expires_at", newExpiry).Error
}
// DeleteSession удаляет сессию по токену
func (r *pgRepository) DeleteSession(token string) error {
return r.db.Where("refresh_token = ?", token).Delete(&account.Session{}).Error
}