179 lines
5.8 KiB
Go
179 lines
5.8 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"ssh-access-tui/internal/keymgr"
|
|
"ssh-access-tui/internal/model"
|
|
)
|
|
|
|
// initConfigUpdateSelect lists every already-issued active user, so the
|
|
// admin can force-refresh anyone's config even if it isn't detected as
|
|
// stale — the status annotation (see viewConfigUpdateSelect) shows which
|
|
// ones actually need it.
|
|
func (m *Model) initConfigUpdateSelect() {
|
|
st := m.loadState()
|
|
m.configUpdateSel = map[string]bool{}
|
|
m.configUpdateOrder = nil
|
|
for _, u := range st.Users {
|
|
if u.Status == model.StatusActive && u.PublicKey != "" {
|
|
m.configUpdateOrder = append(m.configUpdateOrder, u.Username)
|
|
}
|
|
}
|
|
m.configUpdateCursor = 0
|
|
m.configUpdateErr = ""
|
|
}
|
|
|
|
func (m Model) updateConfigUpdateSelect(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 "up", "k":
|
|
if m.configUpdateCursor > 0 {
|
|
m.configUpdateCursor--
|
|
}
|
|
return m, nil
|
|
case "down", "j":
|
|
if m.configUpdateCursor < len(m.configUpdateOrder)-1 {
|
|
m.configUpdateCursor++
|
|
}
|
|
return m, nil
|
|
case " ":
|
|
if len(m.configUpdateOrder) > 0 {
|
|
u := m.configUpdateOrder[m.configUpdateCursor]
|
|
m.configUpdateSel[u] = !m.configUpdateSel[u]
|
|
}
|
|
return m, nil
|
|
case "a":
|
|
for _, u := range m.configUpdateOrder {
|
|
m.configUpdateSel[u] = true
|
|
}
|
|
return m, nil
|
|
case "enter":
|
|
var chosen []string
|
|
for _, u := range m.configUpdateOrder {
|
|
if m.configUpdateSel[u] {
|
|
chosen = append(chosen, u)
|
|
}
|
|
}
|
|
if len(chosen) == 0 {
|
|
m.err = "выберите хотя бы одного пользователя (space)"
|
|
return m, nil
|
|
}
|
|
return m.runConfigUpdate(chosen)
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m Model) viewConfigUpdateSelect() string {
|
|
st := m.loadState()
|
|
body := "Обновить конфиги — выберите пользователей (ключи не трогает, общий пароль на партию)\n\n"
|
|
if len(m.configUpdateOrder) == 0 {
|
|
body += warnStyle.Render("Нет пользователей с уже выданным ключом.") + "\n"
|
|
}
|
|
for i, uname := range m.configUpdateOrder {
|
|
cursor := " "
|
|
if i == m.configUpdateCursor {
|
|
cursor = "> "
|
|
}
|
|
check := "[ ]"
|
|
if m.configUpdateSel[uname] {
|
|
check = "[x]"
|
|
}
|
|
status := okStyle.Render("конфиг актуален")
|
|
if u := st.FindUser(uname); u != nil && st.IsConfigStale(u) {
|
|
status = warnStyle.Render("конфиг устарел — доступ изменился")
|
|
}
|
|
body += fmt.Sprintf("%s%s %-20s %s\n", cursor, check, uname, status)
|
|
}
|
|
body += "\n[space] отметить [a] отметить всех [enter] сгенерировать [esc] назад"
|
|
return borderStyle.Render(body)
|
|
}
|
|
|
|
// runConfigUpdate is the lightweight alternative to a full export: when
|
|
// only group membership or the catalog's services changed, server-side
|
|
// access is already updated immediately (authorized_keys permitopen=) — this
|
|
// just hands the selected users a fresh client config reflecting that,
|
|
// without touching any key. See keymgr.BuildConfigOnlyArchive. Not
|
|
// destructive (no key is rotated, nothing existing breaks), so it runs
|
|
// without a confirmation screen regardless of batch size.
|
|
func (m Model) runConfigUpdate(usernames []string) (tea.Model, tea.Cmd) {
|
|
password, err := keymgr.GenerateBatchPassword(20)
|
|
if err != nil {
|
|
m.err = err.Error()
|
|
return m, nil
|
|
}
|
|
|
|
st := m.loadState()
|
|
var results []keymgr.ConfigOnlyResult
|
|
var failures []string
|
|
|
|
for _, username := range usernames {
|
|
u := st.FindUser(username)
|
|
if u == nil {
|
|
failures = append(failures, username+": не найден в состоянии")
|
|
continue
|
|
}
|
|
if u.PublicKey == "" {
|
|
failures = append(failures, username+": ещё нет ключа, используйте «Экспорт архивов»")
|
|
continue
|
|
}
|
|
forwards := st.AllowedForwards(u)
|
|
res, err := keymgr.BuildConfigOnlyArchive(m.cfg.ScratchDir, m.cfg.ExportDir, m.cfg.JumpHost, m.cfg.JumpPort, username, password, forwards)
|
|
if err != nil {
|
|
failures = append(failures, username+": "+err.Error())
|
|
continue
|
|
}
|
|
u.ForwardsHash = model.HashForwards(forwards)
|
|
results = append(results, *res)
|
|
m.logAudit(username, "admin", "config-only update issued (key unchanged)")
|
|
}
|
|
|
|
if err := m.st.WithLock(func() error {
|
|
return m.st.Save(st, fmt.Sprintf("config-update: обновлены конфиги для %v", usernames))
|
|
}); err != nil {
|
|
failures = append(failures, "сохранение состояния: "+err.Error())
|
|
}
|
|
|
|
m.configUpdateRes = results
|
|
m.configUpdatePass = password
|
|
m.configUpdateErr = ""
|
|
for _, f := range failures {
|
|
m.configUpdateErr += f + "\n"
|
|
}
|
|
m.screen = scrConfigUpdateResult
|
|
return m, nil
|
|
}
|
|
|
|
func (m Model) updateConfigUpdateResult(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
if key, ok := msg.(tea.KeyMsg); ok {
|
|
if key.String() == "esc" || key.String() == "enter" {
|
|
m.screen = scrMenu
|
|
m.err, m.status = "", ""
|
|
return m, nil
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m Model) viewConfigUpdateResult() string {
|
|
body := "Готово — ключи НЕ менялись, обновлён только config.\n\n"
|
|
body += "Пароль архива (один на всю партию, покажется только сейчас!):\n\n"
|
|
body += titleStyle.Render(m.configUpdatePass) + "\n\n"
|
|
body += warnStyle.Render("Сообщите пароль каждому получателю отдельным от архива каналом.") + "\n\n"
|
|
for _, r := range m.configUpdateRes {
|
|
body += fmt.Sprintf(" %-20s -> %s\n", r.Username, r.ArchivePath)
|
|
}
|
|
if m.configUpdateErr != "" {
|
|
body += "\n" + errStyle.Render("Ошибки:\n"+m.configUpdateErr)
|
|
}
|
|
body += "\n[enter/esc] в меню"
|
|
return borderStyle.Render(body)
|
|
}
|