Files
ssh-access-tui/internal/ui/users.go
T
2026-07-22 14:14:53 +03:00

263 lines
7.7 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 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)
}