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"})
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ type Bot struct {
|
||||
webAppURL string
|
||||
|
||||
// UI Elements (Menus)
|
||||
menuMain *tele.ReplyMarkup
|
||||
// menuMain удаляем как статическое поле, так как оно теперь динамическое
|
||||
menuServers *tele.ReplyMarkup
|
||||
menuDicts *tele.ReplyMarkup
|
||||
menuBalance *tele.ReplyMarkup
|
||||
@@ -89,21 +89,8 @@ func NewBot(
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// initMenus инициализирует статические кнопки
|
||||
// initMenus инициализирует статические кнопки (кроме Главного меню)
|
||||
func (bot *Bot) initMenus() {
|
||||
// --- MAIN MENU ---
|
||||
bot.menuMain = &tele.ReplyMarkup{}
|
||||
btnServers := bot.menuMain.Data("🖥 Серверы", "nav_servers")
|
||||
btnDicts := bot.menuMain.Data("🔄 Справочники", "nav_dicts")
|
||||
btnBalance := bot.menuMain.Data("💰 Баланс", "nav_balance")
|
||||
btnApp := bot.menuMain.WebApp("📱 Открыть приложение", &tele.WebApp{URL: bot.webAppURL})
|
||||
|
||||
bot.menuMain.Inline(
|
||||
bot.menuMain.Row(btnServers, btnDicts),
|
||||
bot.menuMain.Row(btnBalance),
|
||||
bot.menuMain.Row(btnApp),
|
||||
)
|
||||
|
||||
// --- SERVERS MENU (Dynamic part logic is in handler) ---
|
||||
bot.menuServers = &tele.ReplyMarkup{}
|
||||
|
||||
@@ -130,7 +117,10 @@ func (bot *Bot) initHandlers() {
|
||||
bot.b.Use(bot.registrationMiddleware)
|
||||
|
||||
// Commands
|
||||
bot.b.Handle("/start", bot.renderMainMenu)
|
||||
bot.b.Handle("/start", bot.handleStartCommand)
|
||||
|
||||
// Admin Commands
|
||||
bot.b.Handle("/admin", bot.handleAdminCommand)
|
||||
|
||||
// Navigation Callbacks
|
||||
bot.b.Handle(&tele.Btn{Unique: "nav_main"}, bot.renderMainMenu)
|
||||
@@ -149,6 +139,7 @@ func (bot *Bot) initHandlers() {
|
||||
})
|
||||
|
||||
// Dynamic Handler for server selection ("set_server_UUID")
|
||||
bot.b.Handle(&tele.Btn{Unique: "adm_list_servers"}, bot.adminListServers)
|
||||
bot.b.Handle(tele.OnCallback, bot.handleCallback)
|
||||
|
||||
// Input Handlers
|
||||
@@ -175,41 +166,203 @@ func (bot *Bot) registrationMiddleware(next tele.HandlerFunc) tele.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// --- RENDERERS (View Layer) ---
|
||||
// handleStartCommand обрабатывает /start и deep linking (приглашения)
|
||||
func (bot *Bot) handleStartCommand(c tele.Context) error {
|
||||
payload := c.Message().Payload // То, что после /start <payload>
|
||||
|
||||
func (bot *Bot) renderMainMenu(c tele.Context) error {
|
||||
// Сбрасываем стейты FSM, если пользователь вернулся в меню
|
||||
bot.fsm.Reset(c.Sender().ID)
|
||||
// Если есть payload, пробуем разобрать как приглашение
|
||||
if payload != "" && strings.HasPrefix(payload, "invite_") {
|
||||
return bot.handleInviteLink(c, strings.TrimPrefix(payload, "invite_"))
|
||||
}
|
||||
|
||||
txt := "👋 <b>Панель управления RMSER</b>\n\n" +
|
||||
"Здесь вы можете управлять подключенными серверами iiko и следить за актуальностью справочников."
|
||||
|
||||
return c.EditOrSend(txt, bot.menuMain, tele.ModeHTML)
|
||||
return bot.renderMainMenu(c)
|
||||
}
|
||||
|
||||
func (bot *Bot) renderServersMenu(c tele.Context) error {
|
||||
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
|
||||
servers, err := bot.accountRepo.GetAllServers(userDB.ID)
|
||||
// handleInviteLink обрабатывает приглашение пользователя на сервер
|
||||
func (bot *Bot) handleInviteLink(c tele.Context, serverIDStr string) error {
|
||||
serverID, err := uuid.Parse(serverIDStr)
|
||||
if err != nil {
|
||||
return c.Send("Ошибка БД: " + err.Error())
|
||||
return c.Send("❌ Некорректная ссылка приглашения.")
|
||||
}
|
||||
|
||||
newUser := c.Sender()
|
||||
// Гарантируем, что юзер есть в БД (хотя middleware это делает, тут для надежности перед логикой)
|
||||
userDB, _ := bot.accountRepo.GetOrCreateUser(newUser.ID, newUser.Username, newUser.FirstName, newUser.LastName)
|
||||
|
||||
// Добавляем пользователя (RoleOperator - желаемая, но репозиторий может оставить более высокую)
|
||||
err = bot.accountRepo.AddUserToServer(serverID, userDB.ID, account.RoleOperator)
|
||||
if err != nil {
|
||||
return c.Send(fmt.Sprintf("❌ Не удалось подключиться к серверу: %v", err))
|
||||
}
|
||||
|
||||
// Сбрасываем кэш подключений
|
||||
bot.rmsFactory.ClearCacheForUser(userDB.ID)
|
||||
|
||||
// Получаем актуальные данные о роли и сервере ПОСЛЕ добавления
|
||||
activeServer, err := bot.accountRepo.GetActiveServer(userDB.ID)
|
||||
if err != nil || activeServer == nil || activeServer.ID != serverID {
|
||||
// Крайний случай, если что-то пошло не так с активацией
|
||||
return c.Send("✅ Доступ предоставлен, но сервер не стал активным автоматически. Выберите его в меню.")
|
||||
}
|
||||
|
||||
role, _ := bot.accountRepo.GetUserRole(userDB.ID, serverID)
|
||||
|
||||
// 1. Отправляем сообщение пользователю
|
||||
c.Send(fmt.Sprintf("✅ Вы подключены к серверу <b>%s</b>.\nВаша роль: <b>%s</b>.\nТеперь вы можете загружать чеки.", activeServer.Name, role), tele.ModeHTML)
|
||||
|
||||
// 2. Уведомляем Владельца (только если это реально новый человек или роль изменилась, но упростим - шлем всегда при переходе по ссылке)
|
||||
// Но не шлем уведомление, если Владелец перешел по своей же ссылке
|
||||
if role != account.RoleOwner {
|
||||
go func() {
|
||||
users, err := bot.accountRepo.GetServerUsers(serverID)
|
||||
if err == nil {
|
||||
for _, u := range users {
|
||||
if u.Role == account.RoleOwner {
|
||||
// Не уведомляем, если это тот же человек (хотя проверка выше role != Owner уже отсекла это, но на всякий случай)
|
||||
if u.UserID == userDB.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
name := newUser.FirstName
|
||||
if newUser.LastName != "" {
|
||||
name += " " + newUser.LastName
|
||||
}
|
||||
if newUser.Username != "" {
|
||||
name += fmt.Sprintf(" (@%s)", newUser.Username)
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("🔔 <b>Обновление команды</b>\n\nПользователь <b>%s</b> активировал приглашение на сервер «%s» (Роль: %s).", name, activeServer.Name, role)
|
||||
|
||||
bot.b.Send(&tele.User{ID: u.User.TelegramID}, msg, tele.ModeHTML)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return bot.renderMainMenu(c)
|
||||
}
|
||||
|
||||
// Реализация интерфейса handlers.Notifier
|
||||
func (bot *Bot) SendRoleChangeNotification(telegramID int64, serverName string, newRole string) {
|
||||
msg := fmt.Sprintf("ℹ️ <b>Изменение прав доступа</b>\n\nСервер: <b>%s</b>\nВаша новая роль: <b>%s</b>", serverName, newRole)
|
||||
bot.b.Send(&tele.User{ID: telegramID}, msg, tele.ModeHTML)
|
||||
}
|
||||
|
||||
func (bot *Bot) SendRemovalNotification(telegramID int64, serverName string) {
|
||||
msg := fmt.Sprintf("⛔ <b>Доступ закрыт</b>\n\nВы были отключены от сервера <b>%s</b>.", serverName)
|
||||
bot.b.Send(&tele.User{ID: telegramID}, msg, tele.ModeHTML)
|
||||
}
|
||||
|
||||
// handleAdminCommand - точка входа в админку
|
||||
func (bot *Bot) handleAdminCommand(c tele.Context) error {
|
||||
userID := c.Sender().ID
|
||||
if _, isAdmin := bot.adminIDs[userID]; !isAdmin {
|
||||
return nil // Игнорируем не админов
|
||||
}
|
||||
|
||||
menu := &tele.ReplyMarkup{}
|
||||
btnServers := menu.Data("🏢 Список серверов", "adm_list_servers")
|
||||
menu.Inline(menu.Row(btnServers))
|
||||
|
||||
return c.Send("🕵️♂️ <b>Super Admin Panel</b>\n\nВыберите действие:", menu, tele.ModeHTML)
|
||||
}
|
||||
|
||||
func (bot *Bot) adminListServers(c tele.Context) error {
|
||||
servers, err := bot.accountRepo.GetAllServersSystemWide()
|
||||
if err != nil {
|
||||
return c.Send("Error: " + err.Error())
|
||||
}
|
||||
|
||||
menu := &tele.ReplyMarkup{}
|
||||
var rows []tele.Row
|
||||
|
||||
// Генерируем кнопки для каждого сервера
|
||||
for _, s := range servers {
|
||||
// adm_srv_<UUID>
|
||||
btn := menu.Data(fmt.Sprintf("🖥 %s", s.Name), "adm_srv_"+s.ID.String())
|
||||
rows = append(rows, menu.Row(btn))
|
||||
}
|
||||
menu.Inline(rows...)
|
||||
|
||||
return c.EditOrSend("<b>Все серверы системы:</b>", menu, tele.ModeHTML)
|
||||
}
|
||||
|
||||
// --- RENDERERS (View Layer) ---
|
||||
|
||||
// renderMainMenu строит меню динамически в зависимости от роли
|
||||
func (bot *Bot) renderMainMenu(c tele.Context) error {
|
||||
bot.fsm.Reset(c.Sender().ID)
|
||||
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
|
||||
activeServer, _ := bot.accountRepo.GetActiveServer(userDB.ID)
|
||||
|
||||
menu := &tele.ReplyMarkup{}
|
||||
|
||||
btnServers := menu.Data("🖥 Серверы", "nav_servers")
|
||||
btnDicts := menu.Data("🔄 Справочники", "nav_dicts")
|
||||
btnBalance := menu.Data("💰 Баланс", "nav_balance")
|
||||
|
||||
var rows []tele.Row
|
||||
rows = append(rows, menu.Row(btnServers, btnDicts))
|
||||
rows = append(rows, menu.Row(btnBalance))
|
||||
|
||||
// Проверяем роль для отображения кнопки App
|
||||
showApp := false
|
||||
if activeServer != nil {
|
||||
role, _ := bot.accountRepo.GetUserRole(userDB.ID, activeServer.ID)
|
||||
if role == account.RoleOwner || role == account.RoleAdmin {
|
||||
showApp = true
|
||||
}
|
||||
}
|
||||
|
||||
if showApp {
|
||||
btnApp := menu.WebApp("📱 Открыть приложение", &tele.WebApp{URL: bot.webAppURL})
|
||||
rows = append(rows, menu.Row(btnApp))
|
||||
} else {
|
||||
// Если оператор или нет сервера, можно добавить подсказку или просто ничего
|
||||
// Для оператора это нормально. Для нового юзера - он пойдет в "Серверы"
|
||||
}
|
||||
|
||||
menu.Inline(rows...)
|
||||
|
||||
txt := "👋 <b>Панель управления RMSER</b>\n\n" +
|
||||
"Здесь вы можете управлять подключенными серверами iiko и следить за актуальностью справочников."
|
||||
|
||||
if activeServer != nil {
|
||||
role, _ := bot.accountRepo.GetUserRole(userDB.ID, activeServer.ID)
|
||||
txt += fmt.Sprintf("\n\nАктивный сервер: <b>%s</b> (%s)", activeServer.Name, role)
|
||||
}
|
||||
|
||||
return c.EditOrSend(txt, menu, tele.ModeHTML)
|
||||
}
|
||||
|
||||
func (bot *Bot) renderServersMenu(c tele.Context) error {
|
||||
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
|
||||
servers, err := bot.accountRepo.GetAllAvailableServers(userDB.ID)
|
||||
if err != nil {
|
||||
return c.Send("Ошибка БД: " + err.Error())
|
||||
}
|
||||
activeServer, _ := bot.accountRepo.GetActiveServer(userDB.ID)
|
||||
|
||||
menu := &tele.ReplyMarkup{}
|
||||
var rows []tele.Row
|
||||
|
||||
for _, s := range servers {
|
||||
icon := "🔴"
|
||||
if s.IsActive {
|
||||
if activeServer != nil && activeServer.ID == s.ID {
|
||||
icon = "🟢"
|
||||
}
|
||||
// Payload: "set_server_<UUID>"
|
||||
btn := menu.Data(fmt.Sprintf("%s %s", icon, s.Name), "set_server_"+s.ID.String())
|
||||
|
||||
// Определяем роль для отображения
|
||||
role, _ := bot.accountRepo.GetUserRole(userDB.ID, s.ID)
|
||||
label := fmt.Sprintf("%s %s (%s)", icon, s.Name, role)
|
||||
|
||||
btn := menu.Data(label, "set_server_"+s.ID.String())
|
||||
rows = append(rows, menu.Row(btn))
|
||||
}
|
||||
|
||||
btnAdd := menu.Data("➕ Добавить сервер", "act_add_server")
|
||||
btnDel := menu.Data("🗑 Удалить", "act_del_server_menu")
|
||||
btnDel := menu.Data("⚙️ Управление / Удаление", "act_del_server_menu")
|
||||
btnBack := menu.Data("🔙 Назад", "nav_main")
|
||||
|
||||
rows = append(rows, menu.Row(btnAdd, btnDel))
|
||||
@@ -228,7 +381,7 @@ func (bot *Bot) renderDictsMenu(c tele.Context) error {
|
||||
|
||||
var txt string
|
||||
if err != nil {
|
||||
txt = fmt.Sprintf("⚠️ <b>Статус:</b> Ошибка (%v)", err)
|
||||
txt = fmt.Sprintf("⚠️ <b>Статус:</b> Ошибка или нет активного сервера (%v)", err)
|
||||
} else {
|
||||
lastUpdate := "—"
|
||||
if stats.LastInvoice != nil {
|
||||
@@ -255,7 +408,6 @@ func (bot *Bot) renderDictsMenu(c tele.Context) error {
|
||||
}
|
||||
|
||||
func (bot *Bot) renderBalanceMenu(c tele.Context) error {
|
||||
// Заглушка баланса
|
||||
txt := "<b>💰 Ваш баланс</b>\n\n" +
|
||||
"💵 Текущий счет: <b>0.00 ₽</b>\n" +
|
||||
"💎 Тариф: <b>Free</b>\n\n" +
|
||||
@@ -268,113 +420,208 @@ func (bot *Bot) renderBalanceMenu(c tele.Context) error {
|
||||
|
||||
func (bot *Bot) handleCallback(c tele.Context) error {
|
||||
data := c.Callback().Data
|
||||
|
||||
// FIX: Telebot v3 добавляет префикс '\f' к Unique ID кнопки.
|
||||
// Нам нужно удалить его, чтобы корректно парсить строку.
|
||||
if len(data) > 0 && data[0] == '\f' {
|
||||
data = data[1:]
|
||||
}
|
||||
|
||||
// Обработка выбора сервера "set_server_..."
|
||||
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
|
||||
|
||||
// --- SELECT SERVER ---
|
||||
if strings.HasPrefix(data, "set_server_") {
|
||||
serverIDStr := strings.TrimPrefix(data, "set_server_")
|
||||
serverIDStr = strings.TrimSpace(serverIDStr)
|
||||
|
||||
// Защита от старых форматов с разделителем |
|
||||
if idx := strings.Index(serverIDStr, "|"); idx != -1 {
|
||||
serverIDStr = serverIDStr[:idx]
|
||||
}
|
||||
|
||||
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
|
||||
|
||||
// 1. Ищем сервер в базе, чтобы убедиться что это сервер этого юзера
|
||||
servers, _ := bot.accountRepo.GetAllServers(userDB.ID)
|
||||
var found bool
|
||||
for _, s := range servers {
|
||||
if s.ID.String() == serverIDStr {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
logger.Log.Warn("User tried to select unknown server",
|
||||
zap.Int64("user_tg_id", c.Sender().ID),
|
||||
zap.String("server_id_req", serverIDStr))
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Сервер не найден или доступ запрещен"})
|
||||
}
|
||||
|
||||
// 2. Делаем активным
|
||||
targetID := parseUUID(serverIDStr)
|
||||
|
||||
if err := bot.accountRepo.SetActiveServer(userDB.ID, targetID); err != nil {
|
||||
logger.Log.Error("Failed to set active server", zap.Error(err))
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Ошибка смены сервера"})
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Ошибка: доступ запрещен"})
|
||||
}
|
||||
|
||||
// 3. Успех
|
||||
bot.rmsFactory.ClearCacheForUser(userDB.ID) // Сброс кэша
|
||||
c.Respond(&tele.CallbackResponse{Text: "✅ Сервер выбран"})
|
||||
return bot.renderServersMenu(c) // Перерисовываем меню
|
||||
|
||||
// Важно: перерисовываем главное меню, чтобы обновилась кнопка App (появилась/пропала)
|
||||
// Но мы находимся в подменю. Логичнее остаться в ServersMenu, но кнопка App в MainMenu.
|
||||
// Пользователь нажмет "Назад" и попадет в MainMenu, где сработает renderMainMenu с новой логикой.
|
||||
return bot.renderServersMenu(c)
|
||||
}
|
||||
|
||||
// --- ЛОГИКА УДАЛЕНИЯ (новая) ---
|
||||
// --- DELETE / LEAVE SERVER ---
|
||||
if strings.HasPrefix(data, "do_del_server_") {
|
||||
serverIDStr := strings.TrimPrefix(data, "do_del_server_")
|
||||
serverIDStr = strings.TrimSpace(serverIDStr)
|
||||
|
||||
// Очистка от мусора
|
||||
if idx := strings.Index(serverIDStr, "|"); idx != -1 {
|
||||
serverIDStr = serverIDStr[:idx]
|
||||
}
|
||||
|
||||
targetID := parseUUID(serverIDStr)
|
||||
if targetID == uuid.Nil {
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Некорректный ID"})
|
||||
|
||||
role, err := bot.accountRepo.GetUserRole(userDB.ID, targetID)
|
||||
if err != nil {
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Ошибка прав доступа"})
|
||||
}
|
||||
|
||||
// 1. Проверяем, активен ли он сейчас
|
||||
// Нам нужно знать это ДО удаления, чтобы переключить активность
|
||||
// Но проще удалить, а потом проверить, остался ли активный сервер
|
||||
|
||||
// Удаляем
|
||||
if err := bot.accountRepo.DeleteServer(targetID); err != nil {
|
||||
logger.Log.Error("Failed to delete server", zap.Error(err))
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Ошибка удаления"})
|
||||
}
|
||||
|
||||
// Сбрасываем кэш клиента в фабрике
|
||||
bot.rmsFactory.ClearCache(targetID)
|
||||
|
||||
// 2. Проверяем, есть ли активный сервер у пользователя
|
||||
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
|
||||
active, err := bot.accountRepo.GetActiveServer(userDB.ID)
|
||||
|
||||
// Если активного нет (мы удалили активный) или ошибка - назначаем новый
|
||||
if active == nil || err != nil {
|
||||
all, _ := bot.accountRepo.GetAllServers(userDB.ID)
|
||||
if len(all) > 0 {
|
||||
// Делаем активным первый попавшийся
|
||||
_ = bot.accountRepo.SetActiveServer(userDB.ID, all[0].ID)
|
||||
c.Respond(&tele.CallbackResponse{Text: "Сервер удален. Активным назначен " + all[0].Name})
|
||||
} else {
|
||||
c.Respond(&tele.CallbackResponse{Text: "Сервер удален. Список пуст."})
|
||||
if role == account.RoleOwner {
|
||||
if err := bot.accountRepo.DeleteServer(targetID); err != nil {
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Ошибка удаления"})
|
||||
}
|
||||
bot.rmsFactory.ClearCacheForUser(userDB.ID)
|
||||
c.Respond(&tele.CallbackResponse{Text: "Сервер полностью удален"})
|
||||
} else {
|
||||
c.Respond(&tele.CallbackResponse{Text: "Сервер удален"})
|
||||
if err := bot.accountRepo.RemoveUserFromServer(targetID, userDB.ID); err != nil {
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Ошибка выхода"})
|
||||
}
|
||||
bot.rmsFactory.ClearCacheForUser(userDB.ID)
|
||||
c.Respond(&tele.CallbackResponse{Text: "Вы покинули сервер"})
|
||||
}
|
||||
|
||||
active, _ := bot.accountRepo.GetActiveServer(userDB.ID)
|
||||
if active == nil {
|
||||
all, _ := bot.accountRepo.GetAllAvailableServers(userDB.ID)
|
||||
if len(all) > 0 {
|
||||
_ = bot.accountRepo.SetActiveServer(userDB.ID, all[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Возвращаемся в меню удаления (обновляем список)
|
||||
return bot.renderDeleteServerMenu(c)
|
||||
}
|
||||
|
||||
// --- INVITE LINK GENERATION ---
|
||||
if strings.HasPrefix(data, "gen_invite_") {
|
||||
serverIDStr := strings.TrimPrefix(data, "gen_invite_")
|
||||
link := fmt.Sprintf("https://t.me/%s?start=invite_%s", bot.b.Me.Username, serverIDStr)
|
||||
c.Respond()
|
||||
return c.Send(fmt.Sprintf("🔗 <b>Ссылка для приглашения:</b>\n\n<code>%s</code>\n\nОтправьте её сотруднику.", link), tele.ModeHTML)
|
||||
}
|
||||
|
||||
// --- ADMIN: SELECT SERVER -> SHOW USERS ---
|
||||
if strings.HasPrefix(data, "adm_srv_") {
|
||||
serverIDStr := strings.TrimPrefix(data, "adm_srv_")
|
||||
serverID := parseUUID(serverIDStr)
|
||||
return bot.renderServerUsers(c, serverID) // <--- ВЫЗОВ НОВОГО МЕТОДА
|
||||
}
|
||||
|
||||
// --- ADMIN: SELECT USER -> CONFIRM OWNERSHIP ---
|
||||
if strings.HasPrefix(data, "adm_usr_") {
|
||||
// Получаем ID связи
|
||||
connIDStr := strings.TrimPrefix(data, "adm_usr_")
|
||||
connID := parseUUID(connIDStr)
|
||||
|
||||
// Загружаем детали связи через новый метод
|
||||
link, err := bot.accountRepo.GetConnectionByID(connID)
|
||||
if err != nil {
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Ошибка: связь не найдена"})
|
||||
}
|
||||
|
||||
// Проверяем роль
|
||||
if link.Role == account.RoleOwner {
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Этот пользователь уже Владелец"})
|
||||
}
|
||||
|
||||
menu := &tele.ReplyMarkup{}
|
||||
|
||||
// ИСПРАВЛЕНИЕ: Для подтверждения тоже передаем ID связи
|
||||
// adm_own_yes_ + UUID = 12 + 36 = 48 байт (OK)
|
||||
btnYes := menu.Data("✅ Сделать Владельцем", fmt.Sprintf("adm_own_yes_%s", link.ID.String()))
|
||||
btnNo := menu.Data("Отмена", "adm_srv_"+link.ServerID.String())
|
||||
|
||||
menu.Inline(menu.Row(btnYes), menu.Row(btnNo))
|
||||
|
||||
txt := fmt.Sprintf("⚠️ <b>Внимание!</b>\n\nВы собираетесь передать права Владельца сервера <b>%s</b> пользователю <b>%s</b>.\n\nТекущий владелец станет Администратором.",
|
||||
link.Server.Name, link.User.FirstName)
|
||||
|
||||
return c.EditOrSend(txt, menu, tele.ModeHTML)
|
||||
}
|
||||
|
||||
// --- ADMIN: EXECUTE TRANSFER ---
|
||||
if strings.HasPrefix(data, "adm_own_yes_") {
|
||||
connIDStr := strings.TrimPrefix(data, "adm_own_yes_")
|
||||
connID := parseUUID(connIDStr)
|
||||
|
||||
link, err := bot.accountRepo.GetConnectionByID(connID)
|
||||
if err != nil {
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Ошибка: связь не найдена"})
|
||||
}
|
||||
|
||||
if err := bot.accountRepo.TransferOwnership(link.ServerID, link.UserID); err != nil {
|
||||
logger.Log.Error("Ownership transfer failed", zap.Error(err))
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Ошибка: " + err.Error()})
|
||||
}
|
||||
|
||||
// Уведомляем нового владельца
|
||||
go func() {
|
||||
msg := fmt.Sprintf("👑 <b>Поздравляем!</b>\n\nВам переданы права Владельца (OWNER) сервера <b>%s</b>.", link.Server.Name)
|
||||
bot.b.Send(&tele.User{ID: link.User.TelegramID}, msg, tele.ModeHTML)
|
||||
}()
|
||||
|
||||
c.Respond(&tele.CallbackResponse{Text: "Успешно!"})
|
||||
|
||||
// Возвращаемся к списку
|
||||
return bot.renderServerUsers(c, link.ServerID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Вспомогательный метод рендера списка пользователей ---
|
||||
func (bot *Bot) renderServerUsers(c tele.Context, serverID uuid.UUID) error {
|
||||
users, err := bot.accountRepo.GetServerUsers(serverID)
|
||||
if err != nil {
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Ошибка загрузки юзеров"})
|
||||
}
|
||||
|
||||
menu := &tele.ReplyMarkup{}
|
||||
var rows []tele.Row
|
||||
|
||||
for _, u := range users {
|
||||
roleIcon := "👤"
|
||||
if u.Role == account.RoleOwner {
|
||||
roleIcon = "👑"
|
||||
}
|
||||
if u.Role == account.RoleAdmin {
|
||||
roleIcon = "⭐️"
|
||||
}
|
||||
|
||||
label := fmt.Sprintf("%s %s %s", roleIcon, u.User.FirstName, u.User.LastName)
|
||||
// Используем ID связи
|
||||
payload := fmt.Sprintf("adm_usr_%s", u.ID.String())
|
||||
|
||||
btn := menu.Data(label, payload)
|
||||
rows = append(rows, menu.Row(btn))
|
||||
}
|
||||
|
||||
btnBack := menu.Data("🔙 К серверам", "adm_list_servers")
|
||||
rows = append(rows, menu.Row(btnBack))
|
||||
menu.Inline(rows...)
|
||||
|
||||
// Для заголовка нам нужно имя сервера, но в users[0].Server оно есть (Preload),
|
||||
// либо если юзеров нет (пустой сервер?), то имя не узнаем без доп запроса.
|
||||
// Но пустой сервер вряд ли будет, там как минимум Owner.
|
||||
serverName := "Unknown"
|
||||
if len(users) > 0 {
|
||||
serverName = users[0].Server.Name
|
||||
}
|
||||
|
||||
return c.EditOrSend(fmt.Sprintf("👥 Пользователи сервера <b>%s</b>:", serverName), menu, tele.ModeHTML)
|
||||
}
|
||||
|
||||
func (bot *Bot) triggerSync(c tele.Context) error {
|
||||
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
|
||||
|
||||
server, err := bot.accountRepo.GetActiveServer(userDB.ID)
|
||||
if err != nil || server == nil {
|
||||
return c.Respond(&tele.CallbackResponse{Text: "Нет активного сервера"})
|
||||
}
|
||||
|
||||
role, _ := bot.accountRepo.GetUserRole(userDB.ID, server.ID)
|
||||
if role == account.RoleOperator {
|
||||
return c.Respond(&tele.CallbackResponse{Text: "⚠️ Синхронизация доступна только Админам", ShowAlert: true})
|
||||
}
|
||||
|
||||
c.Respond(&tele.CallbackResponse{Text: "Запускаю синхронизацию..."})
|
||||
|
||||
// Запускаем в фоне, но уведомляем юзера
|
||||
go func() {
|
||||
if err := bot.syncService.SyncAllData(userDB.ID); err != nil {
|
||||
logger.Log.Error("Manual sync failed", zap.Error(err))
|
||||
@@ -388,10 +635,9 @@ func (bot *Bot) triggerSync(c tele.Context) error {
|
||||
}
|
||||
|
||||
// --- FSM: ADD SERVER FLOW ---
|
||||
|
||||
func (bot *Bot) startAddServerFlow(c tele.Context) error {
|
||||
bot.fsm.SetState(c.Sender().ID, StateAddServerURL)
|
||||
return c.EditOrSend("🔗 Введите <b>URL</b> вашего сервера iikoRMS.\nПример: <code>https://iiko.myrest.ru:443</code>\n\n(Напишите 'отмена' для выхода)", tele.ModeHTML)
|
||||
return c.EditOrSend("🔗 Введите <b>URL</b> вашего сервера iikoRMS.\nПример: <code>https://resto.iiko.it</code>\n\n(Напишите 'отмена' для выхода)", tele.ModeHTML)
|
||||
}
|
||||
|
||||
func (bot *Bot) handleText(c tele.Context) error {
|
||||
@@ -399,7 +645,6 @@ func (bot *Bot) handleText(c tele.Context) error {
|
||||
state := bot.fsm.GetState(userID)
|
||||
text := strings.TrimSpace(c.Text())
|
||||
|
||||
// Глобальная отмена
|
||||
if strings.ToLower(text) == "отмена" || strings.ToLower(text) == "/cancel" {
|
||||
bot.fsm.Reset(userID)
|
||||
return bot.renderMainMenu(c)
|
||||
@@ -432,14 +677,12 @@ func (bot *Bot) handleText(c tele.Context) error {
|
||||
ctx := bot.fsm.GetContext(userID)
|
||||
msg, _ := bot.b.Send(c.Sender(), "⏳ Проверяю подключение...")
|
||||
|
||||
// 1. Проверяем авторизацию (креды)
|
||||
tempClient := bot.rmsFactory.CreateClientFromRawCredentials(ctx.TempURL, ctx.TempLogin, password)
|
||||
if err := tempClient.Auth(); err != nil {
|
||||
bot.b.Delete(msg)
|
||||
return c.Send(fmt.Sprintf("❌ Ошибка авторизации: %v\nПроверьте логин/пароль.", err))
|
||||
}
|
||||
|
||||
// 2. Пробуем узнать имя сервера
|
||||
var detectedName string
|
||||
info, err := rms.GetServerInfo(ctx.TempURL)
|
||||
if err == nil && info.ServerName != "" {
|
||||
@@ -448,30 +691,24 @@ func (bot *Bot) handleText(c tele.Context) error {
|
||||
|
||||
bot.b.Delete(msg)
|
||||
|
||||
// Сохраняем пароль во временный контекст, он нам пригодится при финальном сохранении
|
||||
bot.fsm.UpdateContext(userID, func(uCtx *UserContext) {
|
||||
uCtx.TempPassword = password
|
||||
uCtx.TempServerName = detectedName
|
||||
})
|
||||
|
||||
// Если имя нашли - предлагаем выбор
|
||||
if detectedName != "" {
|
||||
bot.fsm.SetState(userID, StateAddServerConfirmName)
|
||||
|
||||
menu := &tele.ReplyMarkup{}
|
||||
btnYes := menu.Data("✅ Да, использовать это имя", "confirm_name_yes")
|
||||
btnNo := menu.Data("✏️ Ввести другое", "confirm_name_no")
|
||||
menu.Inline(menu.Row(btnYes), menu.Row(btnNo))
|
||||
|
||||
return c.Send(fmt.Sprintf("🔎 Обнаружено имя сервера: <b>%s</b>.\nИспользовать его?", detectedName), menu, tele.ModeHTML)
|
||||
}
|
||||
|
||||
// Если имя не нашли - просим ввести вручную
|
||||
bot.fsm.SetState(userID, StateAddServerInputName)
|
||||
return c.Send("🏷 Введите <b>название</b> для этого сервера (для вашего удобства):")
|
||||
return c.Send("🏷 Введите <b>название</b> для этого сервера:")
|
||||
|
||||
case StateAddServerInputName:
|
||||
// Пользователь ввел свое название
|
||||
name := text
|
||||
if len(name) < 3 {
|
||||
return c.Send("⚠️ Название слишком короткое.")
|
||||
@@ -490,7 +727,7 @@ func (bot *Bot) handlePhoto(c tele.Context) error {
|
||||
|
||||
_, err = bot.rmsFactory.GetClientForUser(userDB.ID)
|
||||
if err != nil {
|
||||
return c.Send("⛔ У вас не настроен сервер iiko.\nИспользуйте /add_server для настройки.")
|
||||
return c.Send("⛔ Ошибка доступа к iiko или сервер не выбран.\nПроверьте статус сервера в меню.")
|
||||
}
|
||||
|
||||
photo := c.Message().Photo
|
||||
@@ -540,17 +777,18 @@ func (bot *Bot) handlePhoto(c tele.Context) error {
|
||||
}
|
||||
|
||||
menu := &tele.ReplyMarkup{}
|
||||
btnOpen := menu.WebApp("📝 Открыть накладную", &tele.WebApp{URL: fullURL})
|
||||
menu.Inline(menu.Row(btnOpen))
|
||||
|
||||
role, _ := bot.accountRepo.GetUserRole(userDB.ID, draft.RMSServerID)
|
||||
if role != account.RoleOperator {
|
||||
btnOpen := menu.WebApp("📝 Открыть накладную", &tele.WebApp{URL: fullURL})
|
||||
menu.Inline(menu.Row(btnOpen))
|
||||
} else {
|
||||
msgText += "\n\n<i>(Редактирование доступно Администратору)</i>"
|
||||
}
|
||||
|
||||
return c.Send(msgText, menu, tele.ModeHTML)
|
||||
}
|
||||
|
||||
func parseUUID(s string) uuid.UUID {
|
||||
id, _ := uuid.Parse(s)
|
||||
return id
|
||||
}
|
||||
|
||||
func (bot *Bot) handleConfirmNameYes(c tele.Context) error {
|
||||
userID := c.Sender().ID
|
||||
ctx := bot.fsm.GetContext(userID)
|
||||
@@ -566,40 +804,34 @@ func (bot *Bot) handleConfirmNameNo(c tele.Context) error {
|
||||
return c.EditOrSend("🏷 Хорошо, введите желаемое <b>название</b>:")
|
||||
}
|
||||
|
||||
// saveServerFinal - общая логика сохранения в БД
|
||||
func (bot *Bot) saveServerFinal(c tele.Context, userID int64, serverName string) error {
|
||||
ctx := bot.fsm.GetContext(userID)
|
||||
|
||||
encPass, _ := bot.cryptoManager.Encrypt(ctx.TempPassword)
|
||||
userDB, _ := bot.accountRepo.GetOrCreateUser(userID, c.Sender().Username, "", "")
|
||||
|
||||
newServer := &account.RMSServer{
|
||||
UserID: userDB.ID,
|
||||
Name: serverName,
|
||||
BaseURL: ctx.TempURL,
|
||||
Login: ctx.TempLogin,
|
||||
EncryptedPassword: encPass,
|
||||
IsActive: true,
|
||||
encPass, _ := bot.cryptoManager.Encrypt(ctx.TempPassword)
|
||||
|
||||
server, err := bot.accountRepo.ConnectServer(userDB.ID, ctx.TempURL, ctx.TempLogin, encPass, serverName)
|
||||
if err != nil {
|
||||
return c.Send("Ошибка подключения сервера: " + err.Error())
|
||||
}
|
||||
|
||||
if err := bot.accountRepo.SaveServer(newServer); err != nil {
|
||||
return c.Send("Ошибка сохранения в БД: " + err.Error())
|
||||
}
|
||||
|
||||
bot.accountRepo.SetActiveServer(userDB.ID, newServer.ID)
|
||||
bot.fsm.Reset(userID)
|
||||
|
||||
c.Send(fmt.Sprintf("✅ Сервер <b>%s</b> успешно добавлен!", serverName), tele.ModeHTML)
|
||||
role, _ := bot.accountRepo.GetUserRole(userDB.ID, server.ID)
|
||||
c.Send(fmt.Sprintf("✅ Сервер <b>%s</b> подключен!\nВаша роль: <b>%s</b>", server.Name, role), tele.ModeHTML)
|
||||
|
||||
// Auto-sync
|
||||
go bot.syncService.SyncAllData(userDB.ID)
|
||||
if role == account.RoleOwner {
|
||||
go bot.syncService.SyncAllData(userDB.ID)
|
||||
} else {
|
||||
go bot.syncService.SyncAllData(userDB.ID)
|
||||
}
|
||||
|
||||
return bot.renderMainMenu(c)
|
||||
}
|
||||
|
||||
func (bot *Bot) renderDeleteServerMenu(c tele.Context) error {
|
||||
userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
|
||||
servers, err := bot.accountRepo.GetAllServers(userDB.ID)
|
||||
servers, err := bot.accountRepo.GetAllAvailableServers(userDB.ID)
|
||||
if err != nil {
|
||||
return c.Send("Ошибка БД: " + err.Error())
|
||||
}
|
||||
@@ -612,10 +844,23 @@ func (bot *Bot) renderDeleteServerMenu(c tele.Context) error {
|
||||
var rows []tele.Row
|
||||
|
||||
for _, s := range servers {
|
||||
// Кнопка удаления для каждого сервера
|
||||
// Префикс do_del_server_
|
||||
btn := menu.Data(fmt.Sprintf("❌ %s", s.Name), "do_del_server_"+s.ID.String())
|
||||
rows = append(rows, menu.Row(btn))
|
||||
role, _ := bot.accountRepo.GetUserRole(userDB.ID, s.ID)
|
||||
|
||||
var label string
|
||||
if role == account.RoleOwner {
|
||||
label = fmt.Sprintf("❌ Удалить %s (Owner)", s.Name)
|
||||
} else {
|
||||
label = fmt.Sprintf("🚪 Покинуть %s", s.Name)
|
||||
}
|
||||
|
||||
btnAction := menu.Data(label, "do_del_server_"+s.ID.String())
|
||||
|
||||
if role == account.RoleOwner || role == account.RoleAdmin {
|
||||
btnInvite := menu.Data(fmt.Sprintf("📩 Invite %s", s.Name), "gen_invite_"+s.ID.String())
|
||||
rows = append(rows, menu.Row(btnAction, btnInvite))
|
||||
} else {
|
||||
rows = append(rows, menu.Row(btnAction))
|
||||
}
|
||||
}
|
||||
|
||||
btnBack := menu.Data("🔙 Назад к списку", "nav_servers")
|
||||
@@ -623,5 +868,10 @@ func (bot *Bot) renderDeleteServerMenu(c tele.Context) error {
|
||||
|
||||
menu.Inline(rows...)
|
||||
|
||||
return c.EditOrSend("🗑 <b>Удаление сервера</b>\n\nНажмите на сервер, который хотите удалить.\nЭто действие нельзя отменить.", menu, tele.ModeHTML)
|
||||
return c.EditOrSend("⚙️ <b>Управление серверами</b>\n\nЗдесь вы можете удалить сервер или пригласить сотрудников.", menu, tele.ModeHTML)
|
||||
}
|
||||
|
||||
func parseUUID(s string) uuid.UUID {
|
||||
id, _ := uuid.Parse(s)
|
||||
return id
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user