This commit is contained in:
redlineo committed 2026-07-22 14:14:53 +03:00
commit 23c7133fa9
33 files changed
+2896

No files matched your search

+262
View File
@@ -0,0 +1,262 @@
package ui
import (
"fmt"
"time"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"ssh-access-tui/internal/model"
"ssh-access-tui/internal/sysadm"
)
func (m *Model) refreshUsersList() {
st := m.loadState()
items := make([]list.Item, 0, len(st.Users))
for _, u := range st.Users {
fp := u.Fingerprint
if fp == "" {
fp = "ключ не выдан"
}
desc := fmt.Sprintf("%s | группы: %v | %s", u.Status, u.Groups, fp)
items = append(items, simpleItem{id: u.Username, title: u.Username, desc: desc})
}
l := list.New(items, list.NewDefaultDelegate(), m.usersList.Width(), m.usersList.Height())
l.Title = "Пользователи [n]новый [l]заблокировать [u]разблокировать [d]удалить [esc]назад"
l.SetShowStatusBar(false)
m.usersList = l
}
func (m Model) updateUsers(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyMsg); ok {
switch key.String() {
case "esc":
m.screen = scrMenu
m.err, m.status = "", ""
return m, nil
case "n":
m.screen = scrUserNew
m.userNewName = textinput.New()
m.userNewName.Placeholder = "имя пользователя (латиница, как логин Linux)"
m.userNewName.Focus()
m.userNewComment = textinput.New()
m.userNewComment.Placeholder = "комментарий (например ФИО) — необязательно"
m.userNewFocus = 0
m.err, m.status = "", ""
return m, nil
case "l", "u", "d":
sel, ok := m.usersList.SelectedItem().(simpleItem)
if !ok {
return m, nil
}
username := sel.id
switch key.String() {
case "l":
if err := sysadm.LockSystemUser(username); err != nil {
m.err = err.Error()
return m, nil
}
_ = sysadm.DisableAuthorizedKeys(username)
m.mutate("заблокирован пользователь "+username, func(st *model.State) error {
u := st.FindUser(username)
if u == nil {
return fmt.Errorf("user %s not found in state", username)
}
now := time.Now()
u.Status = model.StatusLocked
u.LockedAt = &now
return nil
})
m.logAudit(username, "admin", "locked")
case "u":
if err := sysadm.UnlockSystemUser(username); err != nil {
m.err = err.Error()
return m, nil
}
m.mutate("разблокирован пользователь "+username, func(st *model.State) error {
u := st.FindUser(username)
if u == nil {
return fmt.Errorf("user %s not found in state", username)
}
u.Status = model.StatusActive
u.LockedAt = nil
if u.PublicKey != "" {
if err := sysadm.WriteAuthorizedKeys(username, u.PublicKey, st.AllowedHosts(u)); err != nil {
return err
}
}
return nil
})
m.logAudit(username, "admin", "unlocked")
case "d":
if err := sysadm.DeleteSystemUser(username); err != nil {
m.err = err.Error()
return m, nil
}
m.mutate("удалён пользователь "+username, func(st *model.State) error {
for i := range st.Users {
if st.Users[i].Username == username {
st.Users = append(st.Users[:i], st.Users[i+1:]...)
break
}
}
return nil
})
m.logAudit(username, "admin", "deleted")
}
m.refreshUsersList()
return m, nil
}
}
var cmd tea.Cmd
m.usersList, cmd = m.usersList.Update(msg)
return m, cmd
}
func (m Model) viewUsers() string {
return borderStyle.Render(m.usersList.View())
}
// --- new user wizard: step 1, username/comment ---
func (m Model) updateUserNew(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyMsg); ok {
switch key.String() {
case "esc":
m.screen = scrUsers
return m, nil
case "tab", "down":
m.userNewFocus = (m.userNewFocus + 1) % 2
m.applyUserNewFocus()
return m, nil
case "enter":
name := m.userNewName.Value()
if name == "" {
m.err = "имя пользователя не может быть пустым"
return m, nil
}
if sysadm.UserExists(name) {
m.err = fmt.Sprintf("системный пользователь %q уже существует", name)
return m, nil
}
st := m.loadState()
if st.FindUser(name) != nil {
m.err = fmt.Sprintf("пользователь %q уже есть в состоянии tool'а", name)
return m, nil
}
m.err = ""
m.userNewGroupsSel = map[string]bool{}
m.userNewGroupsOrder = nil
for _, g := range st.Groups {
m.userNewGroupsOrder = append(m.userNewGroupsOrder, g.Name)
}
m.userNewCursor = 0
m.screen = scrUserNewGroups
return m, nil
}
}
var cmd tea.Cmd
if m.userNewFocus == 0 {
m.userNewName, cmd = m.userNewName.Update(msg)
} else {
m.userNewComment, cmd = m.userNewComment.Update(msg)
}
return m, cmd
}
func (m *Model) applyUserNewFocus() {
if m.userNewFocus == 0 {
m.userNewName.Focus()
m.userNewComment.Blur()
} else {
m.userNewName.Blur()
m.userNewComment.Focus()
}
}
func (m Model) viewUserNew() string {
body := fmt.Sprintf("Новый пользователь\n\nИмя: %s\nКомментарий: %s\n\n[tab] переключить поле [enter] далее: выбор групп [esc] отмена",
m.userNewName.View(), m.userNewComment.View())
return borderStyle.Render(body)
}
// --- new user wizard: step 2, group checkboxes ---
func (m Model) updateUserNewGroups(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyMsg); ok {
switch key.String() {
case "esc":
m.screen = scrUserNew
return m, nil
case "up", "k":
if m.userNewCursor > 0 {
m.userNewCursor--
}
return m, nil
case "down", "j":
if m.userNewCursor < len(m.userNewGroupsOrder)-1 {
m.userNewCursor++
}
return m, nil
case " ":
if len(m.userNewGroupsOrder) > 0 {
g := m.userNewGroupsOrder[m.userNewCursor]
m.userNewGroupsSel[g] = !m.userNewGroupsSel[g]
}
return m, nil
case "enter":
username := m.userNewName.Value()
comment := m.userNewComment.Value()
var groups []string
for _, g := range m.userNewGroupsOrder {
if m.userNewGroupsSel[g] {
groups = append(groups, g)
}
}
if err := sysadm.CreateSystemUser(username); err != nil {
m.err = err.Error()
m.screen = scrUsers
m.refreshUsersList()
return m, nil
}
m.mutate("создан пользователь "+username, func(st *model.State) error {
st.Users = append(st.Users, model.User{
Username: username,
Groups: groups,
Status: model.StatusActive,
Comment: comment,
CreatedAt: time.Now(),
})
return nil
})
m.logAudit(username, "admin", "created, groups="+fmt.Sprint(groups))
m.status = fmt.Sprintf("пользователь %s создан. Ключ ещё не выдан — сгенерируйте его в разделе «Экспорт архивов».", username)
m.screen = scrUsers
m.refreshUsersList()
return m, nil
}
}
return m, nil
}
func (m Model) viewUserNewGroups() string {
body := "Группы доступа для нового пользователя (union хостов всех выбранных групп)\n\n"
if len(m.userNewGroupsOrder) == 0 {
body += warnStyle.Render("Группы ещё не созданы — сначала создайте хотя бы одну в разделе «Группы и хосты».") + "\n"
}
for i, g := range m.userNewGroupsOrder {
cursor := " "
if i == m.userNewCursor {
cursor = "> "
}
check := "[ ]"
if m.userNewGroupsSel[g] {
check = "[x]"
}
body += fmt.Sprintf("%s%s %s\n", cursor, check, g)
}
body += "\n[space] отметить [enter] создать пользователя [esc] назад"
return borderStyle.Render(body)
}