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

170 lines
5.0 KiB
Go

package ui
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"ssh-access-tui/internal/keymgr"
"ssh-access-tui/internal/model"
"ssh-access-tui/internal/sysadm"
)
func (m *Model) initExportSelect() {
st := m.loadState()
m.exportSel = map[string]bool{}
m.exportOrder = nil
for _, u := range st.Users {
if u.Status == model.StatusActive {
m.exportOrder = append(m.exportOrder, u.Username)
}
}
m.exportCursor = 0
m.exportErr = ""
}
func (m Model) updateExportSelect(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.exportCursor > 0 {
m.exportCursor--
}
return m, nil
case "down", "j":
if m.exportCursor < len(m.exportOrder)-1 {
m.exportCursor++
}
return m, nil
case " ":
if len(m.exportOrder) > 0 {
u := m.exportOrder[m.exportCursor]
m.exportSel[u] = !m.exportSel[u]
}
return m, nil
case "a":
for _, u := range m.exportOrder {
m.exportSel[u] = true
}
return m, nil
case "enter":
var chosen []string
for _, u := range m.exportOrder {
if m.exportSel[u] {
chosen = append(chosen, u)
}
}
if len(chosen) == 0 {
m.err = "выберите хотя бы одного пользователя (space)"
return m, nil
}
return m.runExport(chosen)
}
}
return m, nil
}
// runExport performs the actual batch export: for each chosen user it
// (re)generates a keypair, rewrites authorized_keys with that user's
// currently-allowed hosts, builds the encrypted archive, and records the new
// public key/fingerprint in state. Re-exporting an existing user rotates
// their key — that is deliberate: it is also how a config update ("new host
// added to their group") gets delivered to the client side.
func (m Model) runExport(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.BuildResult
var failures []string
for _, username := range usernames {
u := st.FindUser(username)
if u == nil {
failures = append(failures, username+": не найден в состоянии")
continue
}
hosts := st.AllowedHosts(u)
res, err := keymgr.BuildUserArchive(m.cfg.ScratchDir, m.cfg.ExportDir, m.cfg.JumpHost, m.cfg.JumpPort, username, password, hosts)
if err != nil {
failures = append(failures, username+": "+err.Error())
continue
}
if err := sysadm.WriteAuthorizedKeys(username, res.PublicKeyLine, hosts); err != nil {
failures = append(failures, username+": authorized_keys: "+err.Error())
continue
}
u.PublicKey = res.PublicKeyLine
u.Fingerprint = res.Fingerprint
results = append(results, *res)
m.logAudit(username, "admin", "key issued/rotated, fingerprint="+res.Fingerprint)
}
if err := m.st.WithLock(func() error {
return m.st.Save(st, fmt.Sprintf("export: выпущены/обновлены ключи для %v", usernames))
}); err != nil {
failures = append(failures, "сохранение состояния: "+err.Error())
}
m.exportRes = results
m.exportPass = password
m.exportErr = ""
for _, f := range failures {
m.exportErr += f + "\n"
}
m.screen = scrExportResult
return m, nil
}
func (m Model) viewExportSelect() string {
body := fmt.Sprintf("Экспорт архивов — выберите пользователей (общий пароль архива на всю партию)\n\n")
if len(m.exportOrder) == 0 {
body += warnStyle.Render("Нет активных пользователей. Сначала создайте пользователя.") + "\n"
}
for i, u := range m.exportOrder {
cursor := " "
if i == m.exportCursor {
cursor = "> "
}
check := "[ ]"
if m.exportSel[u] {
check = "[x]"
}
body += fmt.Sprintf("%s%s %s\n", cursor, check, u)
}
body += "\n[space] отметить [a] отметить всех [enter] сгенерировать архивы [esc] назад"
return borderStyle.Render(body)
}
func (m Model) updateExportResult(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) viewExportResult() string {
body := "Готово. Пароль архива (один на всю партию, покажется только сейчас!):\n\n"
body += titleStyle.Render(m.exportPass) + "\n\n"
body += warnStyle.Render("Сообщите этот пароль каждому получателю отдельным каналом (не тем же, что архив).") + "\n\n"
for _, r := range m.exportRes {
body += fmt.Sprintf(" %-20s -> %s (fingerprint %s)\n", r.Username, r.ArchivePath, r.Fingerprint)
}
if m.exportErr != "" {
body += "\n" + errStyle.Render("Ошибки:\n"+m.exportErr)
}
body += "\n[enter/esc] в меню"
return borderStyle.Render(body)
}