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/internal/models"
|
||||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
||||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -155,7 +154,7 @@ func runAddHost(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize storage
|
// Initialize storage
|
||||||
store, err := storage.NewJSONStorage(cfg.GetDataDir())
|
store, err := newStorage(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
return fmt.Errorf("failed to initialize storage: %w", err)
|
||||||
}
|
}
|
||||||
@@ -278,7 +277,7 @@ func addHostInteractive(cfg *config.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize storage
|
// Initialize storage
|
||||||
store, err := storage.NewJSONStorage(cfg.GetDataDir())
|
store, err := newStorage(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
return fmt.Errorf("failed to initialize storage: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
||||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var deleteForce bool
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
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/internal/models"
|
||||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
||||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -69,7 +68,7 @@ func runListHosts(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize storage
|
// Initialize storage
|
||||||
store, err := storage.NewJSONStorage(cfg.GetDataDir())
|
store, err := newStorage(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize storage: %w", err)
|
return fmt.Errorf("failed to initialize storage: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-4
@@ -7,13 +7,15 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
"git.tukangketik.id/swanadiva/hostkeeper/pkg/config"
|
||||||
|
"git.tukangketik.id/swanadiva/hostkeeper/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfgFile string
|
cfgFile string
|
||||||
appCfg *config.Config
|
appCfg *config.Config
|
||||||
verbose int
|
verbose int
|
||||||
debug bool
|
debug bool
|
||||||
|
passwordFlag string
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
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().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().CountVarP(&verbose, "verbose", "v", "verbose output (-v for info, -vv for debug)")
|
||||||
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug mode")
|
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug mode")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&passwordFlag, "password", "", "master password for encrypted storage")
|
||||||
|
|
||||||
rootCmd.AddCommand(versionCmd)
|
rootCmd.AddCommand(versionCmd)
|
||||||
}
|
}
|
||||||
@@ -85,3 +88,15 @@ var versionCmd = &cobra.Command{
|
|||||||
func Execute() error {
|
func Execute() error {
|
||||||
return rootCmd.Execute()
|
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)
|
fmt.Printf("Warning: failed to load known_hosts: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
// Auto-detect encrypted data files
|
||||||
hosts, err := store.ListHosts(ctx)
|
needPassword := encryptFlag || store.IsDataEncrypted()
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load hosts: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
model := tui.New()
|
model := tui.New()
|
||||||
model.SetDataDir(cfg.GetDataDir())
|
model.SetDataDir(cfg.GetDataDir())
|
||||||
model.LoadHosts(hosts)
|
|
||||||
if kh != nil {
|
if kh != nil {
|
||||||
model.SetKnownHosts(kh)
|
model.SetKnownHosts(kh)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If --encrypt flag, show password prompt first
|
if needPassword {
|
||||||
if encryptFlag {
|
// Show password prompt first — hosts will be loaded after password is set
|
||||||
model.ShowEncryptPrompt()
|
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)
|
p := tea.NewProgram(model)
|
||||||
|
|||||||
@@ -52,6 +52,16 @@ func (s *JSONStorage) IsEncrypted() bool {
|
|||||||
return s.password != ""
|
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 {
|
func (s *JSONStorage) ensureDataFiles() error {
|
||||||
files := map[string]string{
|
files := map[string]string{
|
||||||
"hosts.json": "hosts",
|
"hosts.json": "hosts",
|
||||||
|
|||||||
+21
-2
@@ -117,9 +117,28 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, cmd
|
return m, cmd
|
||||||
|
|
||||||
case passwordSetMsg:
|
case passwordSetMsg:
|
||||||
// Password was set — store it and proceed to host list
|
// Password was set — store it and load hosts
|
||||||
m.storagePassword = msg.password
|
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
|
return m, nil
|
||||||
|
|
||||||
case openEncryptPromptMsg:
|
case openEncryptPromptMsg:
|
||||||
|
|||||||
Reference in New Issue
Block a user