feat: auto-detect encrypted files + CLI --password flag
- Storage.IsDataEncrypted() checks if hosts.json is encrypted - TUI auto-detects encrypted files, prompts password automatically - Root command: --password flag for all CLI commands - newStorage() helper applies password flag to storage - add/list/edit/delete commands now support encrypted storage - passwordSetMsg loads hosts after password is set
This commit is contained in:
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/internal/models"
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -155,7 +154,7 @@ func runAddHost(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Initialize storage
|
||||
store, err := storage.NewJSONStorage(cfg.GetDataDir())
|
||||
store, err := newStorage(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
||||
}
|
||||
@@ -278,7 +277,7 @@ func addHostInteractive(cfg *config.Config) error {
|
||||
}
|
||||
|
||||
// Initialize storage
|
||||
store, err := storage.NewJSONStorage(cfg.GetDataDir())
|
||||
store, err := newStorage(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
|
||||
)
|
||||
|
||||
var deleteForce bool
|
||||
@@ -49,7 +48,7 @@ func runDeleteHost(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := storage.NewJSONStorage(cfg.GetDataDir())
|
||||
store, err := newStorage(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func runEditHost(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
store, err := storage.NewJSONStorage(cfg.GetDataDir())
|
||||
store, err := newStorage(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/internal/models"
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -69,7 +68,7 @@ func runListHosts(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Initialize storage
|
||||
store, err := storage.NewJSONStorage(cfg.GetDataDir())
|
||||
store, err := newStorage(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
||||
}
|
||||
|
||||
+19
-4
@@ -7,13 +7,15 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
appCfg *config.Config
|
||||
verbose int
|
||||
debug bool
|
||||
cfgFile string
|
||||
appCfg *config.Config
|
||||
verbose int
|
||||
debug bool
|
||||
passwordFlag string
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -47,6 +49,7 @@ func init() {
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is platform-specific app dir)")
|
||||
rootCmd.PersistentFlags().CountVarP(&verbose, "verbose", "v", "verbose output (-v for info, -vv for debug)")
|
||||
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug mode")
|
||||
rootCmd.PersistentFlags().StringVar(&passwordFlag, "password", "", "master password for encrypted storage")
|
||||
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
@@ -85,3 +88,15 @@ var versionCmd = &cobra.Command{
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
// newStorage creates a new JSONStorage with the password flag applied
|
||||
func newStorage(cfg *config.Config) (*storage.JSONStorage, error) {
|
||||
store, err := storage.NewJSONStorage(cfg.GetDataDir())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if passwordFlag != "" {
|
||||
store.SetPassword(passwordFlag)
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
+12
-8
@@ -49,22 +49,26 @@ func runTUI(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Warning: failed to load known_hosts: %v\n", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
hosts, err := store.ListHosts(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load hosts: %w", err)
|
||||
}
|
||||
// Auto-detect encrypted data files
|
||||
needPassword := encryptFlag || store.IsDataEncrypted()
|
||||
|
||||
model := tui.New()
|
||||
model.SetDataDir(cfg.GetDataDir())
|
||||
model.LoadHosts(hosts)
|
||||
if kh != nil {
|
||||
model.SetKnownHosts(kh)
|
||||
}
|
||||
|
||||
// If --encrypt flag, show password prompt first
|
||||
if encryptFlag {
|
||||
if needPassword {
|
||||
// Show password prompt first — hosts will be loaded after password is set
|
||||
model.ShowEncryptPrompt()
|
||||
} else {
|
||||
// No encryption — load hosts normally
|
||||
ctx := context.Background()
|
||||
hosts, err := store.ListHosts(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load hosts: %w", err)
|
||||
}
|
||||
model.LoadHosts(hosts)
|
||||
}
|
||||
|
||||
p := tea.NewProgram(model)
|
||||
|
||||
@@ -52,6 +52,16 @@ func (s *JSONStorage) IsEncrypted() bool {
|
||||
return s.password != ""
|
||||
}
|
||||
|
||||
// IsDataEncrypted checks if the data files are actually encrypted
|
||||
func (s *JSONStorage) IsDataEncrypted() bool {
|
||||
path := filepath.Join(s.dataDir, "hosts.json")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return crypto.IsEncrypted(string(data))
|
||||
}
|
||||
|
||||
func (s *JSONStorage) ensureDataFiles() error {
|
||||
files := map[string]string{
|
||||
"hosts.json": "hosts",
|
||||
|
||||
+21
-2
@@ -117,9 +117,28 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
|
||||
case passwordSetMsg:
|
||||
// Password was set — store it and proceed to host list
|
||||
// Password was set — store it and load hosts
|
||||
m.storagePassword = msg.password
|
||||
// Close the password prompt tab, host list should already be there
|
||||
|
||||
// Load hosts with the password
|
||||
store, err := storage.NewJSONStorage(m.dataDir)
|
||||
if err == nil {
|
||||
store.SetPassword(msg.password)
|
||||
ctx := context.Background()
|
||||
hosts, loadErr := store.ListHosts(ctx)
|
||||
if loadErr == nil {
|
||||
m.Hosts = hosts
|
||||
// Update host list tab if it exists
|
||||
if hl, ok := m.tabs.Active().(*HostListTab); ok {
|
||||
hl.SetHosts(hosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close the password prompt tab
|
||||
if m.tabs.Len() > 1 {
|
||||
m.tabs.CloseActive()
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case openEncryptPromptMsg:
|
||||
|
||||
Reference in New Issue
Block a user