Sprint 6: JSON file persistence + real SSH terminal + V1 crypto/knownhosts integration
- go.mod replace directive for git.tukangketik.id/swanadiva/hostkeeper → ../../v1 - store/ package: JSON file persistence for hosts, snippets, keys, settings (~/Library/Application Support/hostkeeper/v2/) - model SSH fields: Hostname, Port, Username, AuthType, Password, KeyID - sshconn/ package: real SSH client via golang.org/x/crypto/ssh - Terminal WS: real SSH connection with fallback to mock shell - Dynamic host list in terminal (server-rendered JSON script tag) - All mock data defaults removed from model packages - Settings persist via store/settings.go
This commit is contained in:
@@ -1,100 +1,22 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Host struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
OS string `json:"os"`
|
||||
Provider string `json:"provider"`
|
||||
Status string `json:"status"` // active | offline
|
||||
Status string `json:"status"`
|
||||
LastSeen string `json:"lastSeen"`
|
||||
Type string `json:"type"` // api | db | edge | web | desktop | server
|
||||
}
|
||||
Type string `json:"type"`
|
||||
|
||||
type HostStore struct {
|
||||
Hosts []Host
|
||||
}
|
||||
|
||||
func NewHostStore() *HostStore {
|
||||
return &HostStore{
|
||||
Hosts: defaultHosts(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HostStore) All() []Host {
|
||||
return s.Hosts
|
||||
}
|
||||
|
||||
func (s *HostStore) Search(q, filter string) []Host {
|
||||
var result []Host
|
||||
for _, h := range s.Hosts {
|
||||
if filter != "" && filter != "all" && h.Status != filter {
|
||||
continue
|
||||
}
|
||||
if q == "" || strings.Contains(strings.ToLower(h.Name), strings.ToLower(q)) ||
|
||||
strings.Contains(strings.ToLower(h.IP), strings.ToLower(q)) ||
|
||||
strings.Contains(strings.ToLower(h.OS), strings.ToLower(q)) {
|
||||
result = append(result, h)
|
||||
}
|
||||
}
|
||||
if result == nil {
|
||||
return []Host{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *HostStore) Delete(id string) {
|
||||
for i, h := range s.Hosts {
|
||||
if h.ID == id {
|
||||
s.Hosts = append(s.Hosts[:i], s.Hosts[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HostStore) Add(name, ip, os, provider, hostType string) Host {
|
||||
h := Host{
|
||||
ID: randString(8),
|
||||
Name: name,
|
||||
IP: ip,
|
||||
OS: os,
|
||||
Provider: provider,
|
||||
Status: "active",
|
||||
LastSeen: time.Now().Format("Jan 2, 2006"),
|
||||
Type: hostType,
|
||||
}
|
||||
s.Hosts = append(s.Hosts, h)
|
||||
return h
|
||||
}
|
||||
|
||||
func contains(s, q string) bool {
|
||||
return strings.Contains(strings.ToLower(s), strings.ToLower(q))
|
||||
}
|
||||
|
||||
func randString(n int) string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func defaultHosts() []Host {
|
||||
return []Host{
|
||||
{ID: "h1", Name: "api-prod-01", IP: "10.0.1.15", OS: "Ubuntu 22.04", Provider: "AWS US-East", Status: "active", LastSeen: "Online", Type: "api"},
|
||||
{ID: "h2", Name: "db-primary-01", IP: "10.0.2.5", OS: "Debian 12", Provider: "AWS US-East", Status: "active", LastSeen: "Online", Type: "db"},
|
||||
{ID: "h3", Name: "web-edge-02", IP: "192.168.1.20", OS: "Alpine 3.19", Provider: "DigitalOcean", Status: "offline", LastSeen: "2 hours ago", Type: "edge"},
|
||||
{ID: "h4", Name: "staging-api-01", IP: "10.0.3.10", OS: "Ubuntu 22.04", Provider: "AWS EU-West", Status: "active", LastSeen: "5 min ago", Type: "api"},
|
||||
{ID: "h5", Name: "dev-db-02", IP: "192.168.1.45", OS: "Fedora 39", Provider: "Hetzner", Status: "offline", LastSeen: "1 day ago", Type: "db"},
|
||||
{ID: "h6", Name: "monitor-01", IP: "10.0.0.5", OS: "Ubuntu 24.04", Provider: "AWS US-East", Status: "active", LastSeen: "Just now", Type: "server"},
|
||||
{ID: "h7", Name: "worker-pool-01", IP: "10.0.4.50", OS: "Debian 12", Provider: "GCP US-Central", Status: "active", LastSeen: "1 min ago", Type: "server"},
|
||||
{ID: "h8", Name: "bastion-host", IP: "54.85.12.7", OS: "Ubuntu 22.04", Provider: "AWS US-East", Status: "offline", LastSeen: "3 days ago", Type: "server"},
|
||||
}
|
||||
Hostname string `json:"hostname"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
AuthType string `json:"authType"` // password | key | both
|
||||
Password string `json:"password,omitempty"`
|
||||
KeyID string `json:"keyId,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,78 +1,11 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Key struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
URL string `json:"url"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Strength string `json:"strength"` // weak | moderate | secure
|
||||
Strength string `json:"strength"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
type KeyStore struct {
|
||||
Keys []Key
|
||||
}
|
||||
|
||||
func NewKeyStore() *KeyStore {
|
||||
return &KeyStore{Keys: defaultKeys()}
|
||||
}
|
||||
|
||||
func (s *KeyStore) All() []Key { return s.Keys }
|
||||
func (s *KeyStore) Search(q string) []Key {
|
||||
if q == "" { return s.Keys }
|
||||
var res []Key
|
||||
for _, k := range s.Keys {
|
||||
if contains(k.Name, q) || contains(k.Username, q) || contains(k.URL, q) {
|
||||
res = append(res, k)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *KeyStore) Add(name, username, url, passphrase string) Key {
|
||||
k := Key{
|
||||
ID: randString(8),
|
||||
Name: name,
|
||||
Username: username,
|
||||
URL: url,
|
||||
Passphrase: passphrase,
|
||||
Strength: calcStrength(passphrase),
|
||||
CreatedAt: time.Now().Format("Jan 2, 2006"),
|
||||
}
|
||||
s.Keys = append(s.Keys, k)
|
||||
return k
|
||||
}
|
||||
|
||||
func (s *KeyStore) Delete(id string) {
|
||||
for i, k := range s.Keys {
|
||||
if k.ID == id { s.Keys = append(s.Keys[:i], s.Keys[i+1:]...); return }
|
||||
}
|
||||
}
|
||||
|
||||
func calcStrength(p string) string {
|
||||
l, d, u, s := 0, 0, 0, 0
|
||||
for _, c := range p {
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z': l++
|
||||
case c >= 'A' && c <= 'Z': u++
|
||||
case c >= '0' && c <= '9': d++
|
||||
default: s++
|
||||
}
|
||||
}
|
||||
if len(p) >= 12 && u >= 1 && d >= 1 && s >= 1 { return "secure" }
|
||||
if len(p) >= 8 && u+d+s >= 2 { return "moderate" }
|
||||
return "weak"
|
||||
}
|
||||
|
||||
func defaultKeys() []Key {
|
||||
return []Key{
|
||||
{ID: "k1", Name: "AWS Prod Root", Username: "ec2-user", URL: "aws-console.amazon.com", Passphrase: "••••••••••", Strength: "secure", CreatedAt: "Mar 15, 2026"},
|
||||
{ID: "k2", Name: "GitHub Deploy", Username: "git", URL: "github.com", Passphrase: "••••••••", Strength: "secure", CreatedAt: "Apr 2, 2026"},
|
||||
{ID: "k3", Name: "Dev DB Access", Username: "admin", URL: "dev-db.internal:5432", Passphrase: "••••••", Strength: "moderate", CreatedAt: "May 10, 2026"},
|
||||
{ID: "k4", Name: "Old Server", Username: "root", URL: "192.168.1.100", Passphrase: "••••", Strength: "weak", CreatedAt: "Jan 5, 2026"},
|
||||
{ID: "k5", Name: "Staging API Key", Username: "api-user", URL: "staging.api.company.com", Passphrase: "•••••••••••", Strength: "secure", CreatedAt: "Jun 1, 2026"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Snippet struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -10,52 +8,3 @@ type Snippet struct {
|
||||
Tags []string `json:"tags"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
type SnippetStore struct {
|
||||
Snippets []Snippet
|
||||
}
|
||||
|
||||
func NewSnippetStore() *SnippetStore {
|
||||
return &SnippetStore{Snippets: defaultSnippets()}
|
||||
}
|
||||
|
||||
func (s *SnippetStore) All() []Snippet { return s.Snippets }
|
||||
func (s *SnippetStore) Search(q string) []Snippet {
|
||||
if q == "" { return s.Snippets }
|
||||
var res []Snippet
|
||||
for _, sn := range s.Snippets {
|
||||
if contains(sn.Name, q) || contains(sn.Content, q) || contains(sn.Language, q) {
|
||||
res = append(res, sn)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *SnippetStore) Add(name, content, language string, tags []string) Snippet {
|
||||
sn := Snippet{
|
||||
ID: randString(8),
|
||||
Name: name,
|
||||
Content: content,
|
||||
Language: language,
|
||||
Tags: tags,
|
||||
CreatedAt: time.Now().Format("Jan 2, 2006"),
|
||||
}
|
||||
s.Snippets = append(s.Snippets, sn)
|
||||
return sn
|
||||
}
|
||||
|
||||
func (s *SnippetStore) Delete(id string) {
|
||||
for i, sn := range s.Snippets {
|
||||
if sn.ID == id { s.Snippets = append(s.Snippets[:i], s.Snippets[i+1:]...); return }
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSnippets() []Snippet {
|
||||
return []Snippet{
|
||||
{ID: "s1", Name: "Nginx Reverse Proxy", Content: "server {\n listen 80;\n server_name example.com;\n location / {\n proxy_pass http://localhost:3000;\n proxy_set_header Host $host;\n }\n}", Language: "nginx", Tags: []string{"nginx", "proxy", "web"}, CreatedAt: "Jun 28, 2026"},
|
||||
{ID: "s2", Name: "Docker Compose Postgres", Content: "version: '3.8'\nservices:\n db:\n image: postgres:16\n environment:\n POSTGRES_DB: app\n POSTGRES_PASSWORD: ${DB_PASS}\n ports:\n - 5432:5432", Language: "yaml", Tags: []string{"docker", "postgres", "db"}, CreatedAt: "Jun 25, 2026"},
|
||||
{ID: "s3", Name: "Health Check Script", Content: "#!/bin/bash\ncurl -sf http://localhost/health || exit 1\necho \"OK\"", Language: "bash", Tags: []string{"monitoring", "bash"}, CreatedAt: "Jun 20, 2026"},
|
||||
{ID: "s4", Name: "System Info", Content: "uname -a\ncat /etc/os-release\nfree -h\ndf -h\nuptime", Language: "bash", Tags: []string{"sysadmin", "diagnostics"}, CreatedAt: "Jun 15, 2026"},
|
||||
{ID: "s5", Name: "ufw Rules", Content: "ufw default deny incoming\nufw default allow outgoing\nufw allow 22/tcp\nufw allow 443/tcp\nufw enable", Language: "bash", Tags: []string{"security", "firewall"}, CreatedAt: "Jun 10, 2026"},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user