This commit is contained in:
redlineo committed 2026-07-22 15:14:44 +03:00
1 parent 23c7133fa9
commit 8c1b93c14e
16 files changed
+1187 -295

No files matched your search

+125 -60
View File
@@ -24,7 +24,7 @@ func (m *Model) refreshUsersList() {
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.Title = "Пользователи [n]новый [g]группы [c]обновить конфиг [l]заблокировать [u]разблокировать [d]удалить [esc]назад"
l.SetShowStatusBar(false)
m.usersList = l
}
@@ -46,68 +46,95 @@ func (m Model) updateUsers(msg tea.Msg) (tea.Model, tea.Cmd) {
m.userNewFocus = 0
m.err, m.status = "", ""
return m, nil
case "l", "u", "d":
case "g":
sel, ok := m.usersList.SelectedItem().(simpleItem)
if !ok {
return m, nil
}
st := m.loadState()
u := st.FindUser(sel.id)
if u == nil {
return m, nil
}
m.userNewGroupsSel = map[string]bool{}
for _, g := range u.Groups {
m.userNewGroupsSel[g] = true
}
m.userNewGroupsOrder = nil
for _, g := range st.Groups {
m.userNewGroupsOrder = append(m.userNewGroupsOrder, g.Name)
}
m.userNewCursor = 0
m.userGroupsEditTarget = sel.id
m.err, m.status = "", ""
m.screen = scrUserNewGroups
return m, nil
case "l":
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.pendingConfirm = confirmLockUser
m.confirmTarget = username
m.confirmReturnTo = scrUsers
m.confirmMessage = fmt.Sprintf(
"Заблокировать пользователя %q?\n\n"+
"Аккаунт будет немедленно заблокирован (пароль отключён, срок действия\n"+
"истёк, ключ в authorized_keys закомментирован) — доступ пропадёт сразу.\n"+
"Это обратимо: позже можно разблокировать клавишей [u].", username)
m.screen = scrConfirmAction
return m, nil
case "u":
sel, ok := m.usersList.SelectedItem().(simpleItem)
if !ok {
return m, nil
}
username := sel.id
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.AllowedForwards(u)); err != nil {
return err
}
}
return nil
})
m.logAudit(username, "admin", "unlocked")
m.refreshUsersList()
return m, nil
case "d":
sel, ok := m.usersList.SelectedItem().(simpleItem)
if !ok {
return m, nil
}
username := sel.id
m.pendingConfirm = confirmDeleteUser
m.confirmTarget = username
m.confirmReturnTo = scrUsers
m.confirmMessage = fmt.Sprintf(
"Удалить пользователя %q НЕОБРАТИМО?\n\n"+
"Будет выполнено userdel -r: системный аккаунт и его домашняя папка\n"+
"удаляются безвозвратно.\n"+
"Если нужно просто временно отключить доступ — используйте блокировку\n"+
"[l] вместо удаления, её можно отменить.", username)
m.screen = scrConfirmAction
return m, nil
case "c":
sel, ok := m.usersList.SelectedItem().(simpleItem)
if !ok {
return m, nil
}
return m.runConfigUpdate(sel.id)
}
}
var cmd tea.Cmd
@@ -184,11 +211,20 @@ func (m Model) viewUserNew() string {
// --- new user wizard: step 2, group checkboxes ---
// updateUserNewGroups drives the group-checkbox screen, shared by two
// flows: assigning groups while creating a brand new user (userGroupsEditTarget
// empty), and changing an existing user's groups later (userGroupsEditTarget
// set to their username, entered via "g" on the users list).
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
if m.userGroupsEditTarget != "" {
m.userGroupsEditTarget = ""
m.screen = scrUsers
} else {
m.screen = scrUserNew
}
return m, nil
case "up", "k":
if m.userNewCursor > 0 {
@@ -207,14 +243,37 @@ func (m Model) updateUserNewGroups(msg tea.Msg) (tea.Model, tea.Cmd) {
}
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 m.userGroupsEditTarget != "" {
username := m.userGroupsEditTarget
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.Groups = groups
if u.PublicKey != "" && u.Status == model.StatusActive {
if err := sysadm.WriteAuthorizedKeys(username, u.PublicKey, st.AllowedForwards(u)); err != nil {
return err
}
}
return nil
})
m.logAudit(username, "admin", "groups changed to "+fmt.Sprint(groups))
m.userGroupsEditTarget = ""
m.screen = scrUsers
m.refreshUsersList()
return m, nil
}
username := m.userNewName.Value()
comment := m.userNewComment.Value()
if err := sysadm.CreateSystemUser(username); err != nil {
m.err = err.Error()
m.screen = scrUsers
@@ -242,7 +301,13 @@ func (m Model) updateUserNewGroups(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (m Model) viewUserNewGroups() string {
body := "Группы доступа для нового пользователя (union хостов всех выбранных групп)\n\n"
title := "Группы доступа для нового пользователя"
confirmHint := "создать пользователя"
if m.userGroupsEditTarget != "" {
title = fmt.Sprintf("Группы доступа пользователя %s", m.userGroupsEditTarget)
confirmHint = "сохранить"
}
body := title + " (эффективный доступ = объединение сервисов всех отмеченных групп)\n\n"
if len(m.userNewGroupsOrder) == 0 {
body += warnStyle.Render("Группы ещё не созданы — сначала создайте хотя бы одну в разделе «Группы и хосты».") + "\n"
}
@@ -257,6 +322,6 @@ func (m Model) viewUserNewGroups() string {
}
body += fmt.Sprintf("%s%s %s\n", cursor, check, g)
}
body += "\n[space] отметить [enter] создать пользователя [esc] назад"
body += fmt.Sprintf("\n[space] отметить [enter] %s [esc] назад", confirmHint)
return borderStyle.Render(body)
}