feat: Sprint 2 — snippets + keychain CRUD, standalone templates, clipboard/utils JS

This commit is contained in:
swanadiva
2026-07-07 13:26:56 +07:00
parent 8cfdec3980
commit c4b0d7d8c0
17 changed files with 691 additions and 112 deletions
+10 -8
View File
@@ -7,6 +7,15 @@ import (
var store = model.NewHostStore()
var navItems = []fiber.Map{
{"ID": "hosts", "Label": "Hosts", "Icon": "server"},
{"ID": "terminal", "Label": "Terminal", "Icon": "terminal"},
{"ID": "sftp", "Label": "SFTP", "Icon": "folder-sync"},
{"ID": "snippets", "Label": "Snippets", "Icon": "code2"},
{"ID": "keychain", "Label": "Keychain", "Icon": "key-round"},
{"ID": "settings", "Label": "Settings", "Icon": "settings2"},
}
func Dashboard(c *fiber.Ctx) error {
hosts := store.All()
return c.Render("index", fiber.Map{
@@ -15,14 +24,7 @@ func Dashboard(c *fiber.Ctx) error {
"Hosts": hosts,
"Active": countByStatus(hosts, "active"),
"Offline": countByStatus(hosts, "offline"),
"NavItems": []fiber.Map{
{"ID": "hosts", "Label": "Hosts", "Icon": "server"},
{"ID": "terminal", "Label": "Terminal", "Icon": "terminal"},
{"ID": "sftp", "Label": "SFTP", "Icon": "folder-sync"},
{"ID": "snippets", "Label": "Snippets", "Icon": "code2"},
{"ID": "keychain", "Label": "Keychain", "Icon": "key-round"},
{"ID": "settings", "Label": "Settings", "Icon": "settings2"},
},
"NavItems": navItems,
})
}
+41
View File
@@ -0,0 +1,41 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model"
)
var keyStore = model.NewKeyStore()
func Keychain(c *fiber.Ctx) error {
keys := keyStore.All()
return c.Render("keychain", fiber.Map{
"View": "keychain",
"Title": "Keychain",
"Keys": keys,
"NavItems": navItems,
})
}
func KeyGrid(c *fiber.Ctx) error {
q := c.Query("q", "")
keys := keyStore.Search(q)
return c.Render("credential_grid", fiber.Map{"Keys": keys})
}
func CreateKey(c *fiber.Ctx) error {
name := c.FormValue("name")
username := c.FormValue("username")
url := c.FormValue("url")
passphrase := c.FormValue("passphrase")
keyStore.Add(name, username, url, passphrase)
keys := keyStore.All()
return c.Render("credential_grid", fiber.Map{"Keys": keys})
}
func DeleteKey(c *fiber.Ctx) error {
id := c.Params("id")
keyStore.Delete(id)
keys := keyStore.All()
return c.Render("credential_grid", fiber.Map{"Keys": keys})
}
+40
View File
@@ -0,0 +1,40 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model"
)
var snippetStore = model.NewSnippetStore()
func Snippets(c *fiber.Ctx) error {
snippets := snippetStore.All()
return c.Render("snippets", fiber.Map{
"View": "snippets",
"Title": "Snippets",
"Snippets": snippets,
"NavItems": navItems,
})
}
func SnippetGrid(c *fiber.Ctx) error {
q := c.Query("q", "")
snippets := snippetStore.Search(q)
return c.Render("snippet_grid", fiber.Map{"Snippets": snippets})
}
func CreateSnippet(c *fiber.Ctx) error {
name := c.FormValue("name")
content := c.FormValue("content")
language := c.FormValue("language")
snippetStore.Add(name, content, language, nil)
snippets := snippetStore.All()
return c.Render("snippet_grid", fiber.Map{"Snippets": snippets})
}
func DeleteSnippet(c *fiber.Ctx) error {
id := c.Params("id")
snippetStore.Delete(id)
snippets := snippetStore.All()
return c.Render("snippet_grid", fiber.Map{"Snippets": snippets})
}
+4
View File
@@ -73,6 +73,10 @@ func (s *HostStore) Add(name, ip, os, provider, hostType string) Host {
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)
+78
View File
@@ -0,0 +1,78 @@
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
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"},
}
}
+61
View File
@@ -0,0 +1,61 @@
package model
import "time"
type Snippet struct {
ID string `json:"id"`
Name string `json:"name"`
Content string `json:"content"`
Language string `json:"language"`
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"},
}
}