add edit and all in one menu for hosts and services
This commit is contained in:
1 parent
8c1b93c14e
commit
e39ad275b5
15 files changed
+1136
-675
No files matched your search
+106
-45
@@ -1,37 +1,96 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"ssh-access-tui/internal/model"
|
||||
"ssh-access-tui/internal/sysadm"
|
||||
)
|
||||
|
||||
// updateConfirmAction drives the single generic "are you sure" screen that
|
||||
// backs every destructive action in the tool (lock/delete a user, delete a
|
||||
// host or a service). Nothing the action does runs until this confirms it;
|
||||
// esc/n cancels with zero effect.
|
||||
const (
|
||||
confirmTokenLetters = "abcdefghijklmnopqrstuvwxyz"
|
||||
confirmTokenDigits = "0123456789"
|
||||
)
|
||||
|
||||
// generateConfirmToken returns a short, freshly random string the admin
|
||||
// must type back exactly to carry out a destructive action — e.g.
|
||||
// "kqzvm-482". Confirming with a fixed keypress (enter/y) can fire from a
|
||||
// reflexive/habitual keypress; typing back a token that's different every
|
||||
// time cannot.
|
||||
func generateConfirmToken() string {
|
||||
letters := make([]byte, 5)
|
||||
for i := range letters {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(confirmTokenLetters))))
|
||||
letters[i] = confirmTokenLetters[n.Int64()]
|
||||
}
|
||||
digits := make([]byte, 3)
|
||||
for i := range digits {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(confirmTokenDigits))))
|
||||
digits[i] = confirmTokenDigits[n.Int64()]
|
||||
}
|
||||
return string(letters) + "-" + string(digits)
|
||||
}
|
||||
|
||||
// startConfirm switches to the generic confirmation screen: kind identifies
|
||||
// what executeConfirmedAction should do once the token is typed back
|
||||
// correctly, target is the entity being acted on (username/host/service/
|
||||
// group name — unused for confirmExportRotate, which instead reads
|
||||
// m.exportChosen), message explains the consequences, and returnTo is where
|
||||
// esc/cancel goes back to.
|
||||
func (m Model) startConfirm(kind confirmKind, target, message string, returnTo screen) Model {
|
||||
m.pendingConfirm = kind
|
||||
m.confirmTarget = target
|
||||
m.confirmMessage = message
|
||||
m.confirmReturnTo = returnTo
|
||||
m.confirmToken = generateConfirmToken()
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "введите строку выше"
|
||||
ti.CharLimit = 32
|
||||
ti.Focus()
|
||||
m.confirmInput = ti
|
||||
m.confirmError = ""
|
||||
m.screen = scrConfirmAction
|
||||
return m
|
||||
}
|
||||
|
||||
func (m Model) updateConfirmAction(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
switch key.String() {
|
||||
case "esc", "n":
|
||||
case "esc":
|
||||
m.pendingConfirm = confirmNone
|
||||
m.screen = m.confirmReturnTo
|
||||
return m, nil
|
||||
case "enter", "y":
|
||||
case "enter":
|
||||
if strings.TrimSpace(m.confirmInput.Value()) != m.confirmToken {
|
||||
m.confirmError = "не совпадает со строкой выше — попробуйте ещё раз (или esc для отмены)"
|
||||
m.confirmInput.SetValue("")
|
||||
return m, nil
|
||||
}
|
||||
return m.executeConfirmedAction()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
var cmd tea.Cmd
|
||||
m.confirmInput, cmd = m.confirmInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m Model) viewConfirmAction() string {
|
||||
body := warnStyle.Render("ТРЕБУЕТСЯ ПОДТВЕРЖДЕНИЕ") + "\n\n"
|
||||
body += m.confirmMessage + "\n\n"
|
||||
body += "[enter/y] подтвердить [esc/n] отмена, ничего не изменится"
|
||||
body += "Чтобы подтвердить, наберите ровно эту строку:\n\n"
|
||||
body += " " + titleStyle.Render(m.confirmToken) + "\n\n"
|
||||
body += m.confirmInput.View() + "\n"
|
||||
if m.confirmError != "" {
|
||||
body += "\n" + errStyle.Render(m.confirmError) + "\n"
|
||||
}
|
||||
body += "\n[enter] проверить [esc] отмена, ничего не изменится"
|
||||
return borderStyle.Render(body)
|
||||
}
|
||||
|
||||
@@ -39,7 +98,6 @@ func (m Model) executeConfirmedAction() (tea.Model, tea.Cmd) {
|
||||
kind := m.pendingConfirm
|
||||
target := m.confirmTarget
|
||||
m.pendingConfirm = confirmNone
|
||||
m.err, m.status = "", ""
|
||||
|
||||
switch kind {
|
||||
case confirmLockUser:
|
||||
@@ -50,6 +108,10 @@ func (m Model) executeConfirmedAction() (tea.Model, tea.Cmd) {
|
||||
return m.doDeleteHost(target)
|
||||
case confirmDeleteService:
|
||||
return m.doDeleteService(target)
|
||||
case confirmDeleteGroup:
|
||||
return m.doDeleteGroup(target)
|
||||
case confirmExportRotate:
|
||||
return m.runExport(m.exportChosen)
|
||||
}
|
||||
m.screen = m.confirmReturnTo
|
||||
return m, nil
|
||||
@@ -99,52 +161,51 @@ func (m Model) doDeleteUser(username string) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// doDeleteHost removes a host (and all its services) from m.selectedGroup,
|
||||
// then resyncs authorized_keys for every affected user.
|
||||
// doDeleteHost removes a catalog host (and its services) entirely, dropping
|
||||
// it from every group that referenced it, and resyncs authorized_keys for
|
||||
// every user that lost access as a result.
|
||||
func (m Model) doDeleteHost(hostName string) (tea.Model, tea.Cmd) {
|
||||
groupName := m.selectedGroup
|
||||
m.mutate(fmt.Sprintf("удалён хост %s из группы %s", hostName, groupName), func(st *model.State) error {
|
||||
g := st.FindGroup(groupName)
|
||||
if g == nil {
|
||||
return fmt.Errorf("group %s not found", groupName)
|
||||
}
|
||||
for i := range g.Hosts {
|
||||
if g.Hosts[i].Name == hostName {
|
||||
g.Hosts = append(g.Hosts[:i], g.Hosts[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
return resyncGroupUsers(st, groupName)
|
||||
m.mutate("удалён хост "+hostName+" из каталога", func(st *model.State) error {
|
||||
affected := st.UsernamesUsingHost(hostName)
|
||||
st.DeleteHost(hostName)
|
||||
return resyncUsernames(st, affected)
|
||||
})
|
||||
m.screen = scrGroupHosts
|
||||
m.refreshHostsList()
|
||||
m.screen = scrCatalogHosts
|
||||
m.refreshCatalogHostsList()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// doDeleteService removes a service from m.selectedHost within
|
||||
// m.selectedGroup, then resyncs authorized_keys for every affected user.
|
||||
// doDeleteService removes a service from m.selectedHost, then resyncs
|
||||
// authorized_keys for every user that lost access as a result.
|
||||
func (m Model) doDeleteService(serviceName string) (tea.Model, tea.Cmd) {
|
||||
groupName, hostName := m.selectedGroup, m.selectedHost
|
||||
hostName := m.selectedHost
|
||||
m.mutate(fmt.Sprintf("удалён сервис %s у хоста %s", serviceName, hostName), func(st *model.State) error {
|
||||
g := st.FindGroup(groupName)
|
||||
if g == nil {
|
||||
return fmt.Errorf("group %s not found", groupName)
|
||||
h := st.FindHost(hostName)
|
||||
if h == nil {
|
||||
return fmt.Errorf("host %s not found", hostName)
|
||||
}
|
||||
for hi := range g.Hosts {
|
||||
if g.Hosts[hi].Name != hostName {
|
||||
continue
|
||||
}
|
||||
svcs := g.Hosts[hi].Services
|
||||
for si := range svcs {
|
||||
if svcs[si].Name == serviceName {
|
||||
g.Hosts[hi].Services = append(svcs[:si], svcs[si+1:]...)
|
||||
break
|
||||
}
|
||||
for i := range h.Services {
|
||||
if h.Services[i].Name == serviceName {
|
||||
h.Services = append(h.Services[:i], h.Services[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
return resyncGroupUsers(st, groupName)
|
||||
return resyncUsernames(st, st.UsernamesUsingHost(hostName))
|
||||
})
|
||||
m.screen = scrHostServices
|
||||
m.refreshServicesList()
|
||||
m.screen = scrCatalogServices
|
||||
m.refreshCatalogServicesList()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// doDeleteGroup removes a group and its membership from every user who had
|
||||
// it, then resyncs authorized_keys for everyone who lost access.
|
||||
func (m Model) doDeleteGroup(groupName string) (tea.Model, tea.Cmd) {
|
||||
m.mutate("удалена группа "+groupName, func(st *model.State) error {
|
||||
affected := st.UsernamesInGroup(groupName)
|
||||
st.DeleteGroup(groupName)
|
||||
return resyncUsernames(st, affected)
|
||||
})
|
||||
m.screen = scrGroups
|
||||
m.refreshGroupsList()
|
||||
return m, nil
|
||||
}
|
||||
Reference in new issue
Block a user