Skip to content

Commit 6ef8c8e

Browse files
committed
refactor: separate storage from data handling, improve error handling
- refactor to use a `Storage` interface, with `JsonStorage` and `MemoryStorage` implementations - improve error handling and input validation across the API and command-line - add and update unit tests
1 parent d1c4cd7 commit 6ef8c8e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+5074
-1338
lines changed

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ linux: frontend
99

1010
test: backend frontend
1111
go test ./...
12-
cd frontend && yarn lint
12+
cd frontend && npm run lint
1313

1414
frontend:
15-
cd frontend && yarn && yarn build && cd ..
15+
cd frontend && npm i && npm run build && cd ..
1616
cp -R frontend/dist cmd/

README.md

Lines changed: 134 additions & 74 deletions
Large diffs are not rendered by default.

api/groups.go

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ import (
88
"github.com/shoobyban/sshman/backend"
99
)
1010

11-
// GroupsHandler handler
11+
// GroupsHandler is a handler for group-related operations.
1212
type GroupsHandler struct {
1313
Prefix string
1414
}
1515

16-
// Config returns a loaded configuration from context
17-
func (h GroupsHandler) Config(r *http.Request) *backend.Storage {
16+
// Config returns a loaded configuration from the context.
17+
func (h GroupsHandler) Config(r *http.Request) *backend.Data {
1818
ctx := r.Context()
19-
if cfg, ok := ctx.Value(ConfigKey).(*backend.Storage); ok {
19+
if cfg, ok := ctx.Value(ConfigKey).(*backend.Data); ok {
2020
return cfg
2121
}
22-
return &backend.Storage{}
22+
return backend.DefaultConfig()
2323
}
2424

25-
// AddRoutes adds group specific routes to the router
25+
// AddRoutes adds group-specific routes to the router.
2626
func (h GroupsHandler) AddRoutes(router *chi.Mux) {
2727
router.Get(h.Prefix, h.GetAllGroups)
2828
router.Get(h.Prefix+"/{id}", h.GetGroupDetails)
@@ -31,24 +31,28 @@ func (h GroupsHandler) AddRoutes(router *chi.Mux) {
3131
router.Post(h.Prefix, h.CreateGroup)
3232
}
3333

34-
// GetAllGroups handler returns all groups
34+
// GetAllGroups is a handler that returns all groups.
3535
func (h GroupsHandler) GetAllGroups(w http.ResponseWriter, r *http.Request) {
36-
json.NewEncoder(w).Encode(h.Config(r).GetGroups())
36+
if err := json.NewEncoder(w).Encode(h.Config(r).GetGroups()); err != nil {
37+
http.Error(w, err.Error(), http.StatusInternalServerError)
38+
}
3739
}
3840

39-
// GetGroupDetails handler returns group details
41+
// GetGroupDetails is a handler that returns group details.
4042
func (h GroupsHandler) GetGroupDetails(w http.ResponseWriter, r *http.Request) {
4143
id := chi.URLParam(r, "id")
42-
json.NewEncoder(w).Encode(h.Config(r).GetGroup(id))
44+
if err := json.NewEncoder(w).Encode(h.Config(r).GetGroup(id)); err != nil {
45+
http.Error(w, err.Error(), http.StatusInternalServerError)
46+
}
4347
}
4448

45-
// CreateGroup handler creates a group and adds users and hosts to it
49+
// CreateGroup is a handler that creates a group and adds users and hosts to it.
4650
// format: label => [hosts: [host1, host2], users: [user1, user2]]
4751
func (h GroupsHandler) CreateGroup(w http.ResponseWriter, r *http.Request) {
4852
h.UpdateGroup(w, r)
4953
}
5054

51-
// UpdateGroup handler updates a group and updates users and hosts binding
55+
// UpdateGroup is a handler that updates a group and its user and host bindings.
5256
// format: label => [hosts: [host1, host2], users: [user1, user2]]
5357
func (h GroupsHandler) UpdateGroup(w http.ResponseWriter, r *http.Request) {
5458
var group struct {
@@ -57,20 +61,29 @@ func (h GroupsHandler) UpdateGroup(w http.ResponseWriter, r *http.Request) {
5761
Hosts []string
5862
}
5963
id := chi.URLParam(r, "id")
60-
json.NewDecoder(r.Body).Decode(&group)
64+
if err := json.NewDecoder(r.Body).Decode(&group); err != nil {
65+
http.Error(w, err.Error(), http.StatusBadRequest)
66+
return
67+
}
68+
if group.Label == "" {
69+
http.Error(w, "Label cannot be empty", http.StatusBadRequest)
70+
return
71+
}
6172
h.Config(r).UpdateGroup(group.Label, group.Hosts, group.Users)
6273
if id != "" && group.Label != id {
6374
h.Config(r).DeleteGroup(id)
6475
}
65-
json.NewEncoder(w).Encode(group)
76+
if err := json.NewEncoder(w).Encode(group); err != nil {
77+
http.Error(w, err.Error(), http.StatusInternalServerError)
78+
}
6679
}
6780

68-
// DeleteGroup handler deletes a group
81+
// DeleteGroup is a handler that deletes a group.
6982
func (h GroupsHandler) DeleteGroup(w http.ResponseWriter, r *http.Request) {
7083
id := chi.URLParam(r, "id")
7184
if h.Config(r).DeleteGroup(id) {
7285
w.WriteHeader(http.StatusNoContent)
7386
} else {
74-
w.WriteHeader(http.StatusNotFound)
87+
http.Error(w, "Group not found", http.StatusNotFound)
7588
}
7689
}

api/groups_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func contains(slice []string, s string) bool {
2121
}
2222

2323
func TestCreateGroup(t *testing.T) {
24-
cfg := backend.NewTestStorage()
24+
cfg := backend.NewData(&backend.MemoryStorage{})
2525
cfg.AddHost(
2626
&backend.Host{Alias: "host1", Host: "host1.com", User: "user1", Groups: []string{"group2"}},
2727
false)
@@ -45,7 +45,7 @@ func TestCreateGroup(t *testing.T) {
4545
}
4646
func TestUpdateGroup(t *testing.T) {
4747

48-
cfg := backend.NewTestStorage()
48+
cfg := backend.NewData(&backend.MemoryStorage{})
4949
cfg.AddHost(
5050
&backend.Host{Alias: "host1", Host: "host1.com", User: "user1", Groups: []string{"group2"}},
5151
false)
@@ -75,7 +75,7 @@ func TestUpdateGroup(t *testing.T) {
7575

7676
func TestDeleteGroup(t *testing.T) {
7777

78-
cfg := backend.NewTestStorage()
78+
cfg := backend.NewData(&backend.MemoryStorage{})
7979
cfg.AddHost(
8080
&backend.Host{Alias: "host1", Host: "host1.com", User: "user1", Groups: []string{"group2"}},
8181
false)

api/hosts.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ import (
88
"github.com/shoobyban/sshman/backend"
99
)
1010

11-
// HostsHandler is a struct for handling hosts
11+
// HostsHandler is a struct for handling hosts.
1212
type HostsHandler struct {
1313
Prefix string
1414
}
1515

16-
// Config returns a loaded configuration for the handler
17-
func (h HostsHandler) Config(r *http.Request) *backend.Storage {
16+
// Config returns a loaded configuration for the handler.
17+
func (h HostsHandler) Config(r *http.Request) backend.Config {
1818
ctx := r.Context()
19-
if cfg, ok := ctx.Value(ConfigKey).(*backend.Storage); ok {
19+
if cfg, ok := ctx.Value(ConfigKey).(*backend.Data); ok {
2020
return cfg
2121
}
22-
return backend.NewStorage(false)
22+
return backend.DefaultConfig()
2323
}
2424

25-
// AddRoutes adds hosthandler specific routes to the router
25+
// AddRoutes adds hosthandler specific routes to the router.
2626
func (h HostsHandler) AddRoutes(router *chi.Mux) {
2727
router.Get(h.Prefix, h.GetAllHosts)
2828
router.Get(h.Prefix+"/{id}", h.GetHostDetails)
@@ -33,23 +33,23 @@ func (h HostsHandler) AddRoutes(router *chi.Mux) {
3333
router.Delete(h.Prefix+"/sync", h.StopSyncHandler)
3434
}
3535

36-
// GetAllHosts returns all hosts
36+
// GetAllHosts returns all hosts.
3737
func (h HostsHandler) GetAllHosts(w http.ResponseWriter, r *http.Request) {
3838
json.NewEncoder(w).Encode(h.Config(r).Hosts())
3939
}
4040

41-
// GetHostDetails returns host details
41+
// GetHostDetails returns host details.
4242
func (h HostsHandler) GetHostDetails(w http.ResponseWriter, r *http.Request) {
4343
id := chi.URLParam(r, "id")
4444
json.NewEncoder(w).Encode(h.Config(r).GetHost(id))
4545
}
4646

47-
// CreateHost creates a new host
47+
// CreateHost creates a new host.
4848
func (h HostsHandler) CreateHost(w http.ResponseWriter, r *http.Request) {
4949
h.UpdateHost(w, r)
5050
}
5151

52-
// UpdateHost updates a host
52+
// UpdateHost updates a host.
5353
func (h HostsHandler) UpdateHost(w http.ResponseWriter, r *http.Request) {
5454
id := chi.URLParam(r, "id")
5555
var host backend.Host
@@ -86,7 +86,7 @@ func (h HostsHandler) UpdateHost(w http.ResponseWriter, r *http.Request) {
8686
json.NewEncoder(w).Encode(host)
8787
}
8888

89-
// DeleteHost deletes a host
89+
// DeleteHost deletes a host.
9090
func (h HostsHandler) DeleteHost(w http.ResponseWriter, r *http.Request) {
9191
id := chi.URLParam(r, "id")
9292
cfg := h.Config(r)

api/hosts_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616

1717
func TestGetAllHosts(t *testing.T) {
1818
// test Hosts.GetAllHosts method
19-
cfg := backend.NewTestStorage()
19+
cfg := backend.NewData(&backend.MemoryStorage{})
2020
cfg.AddHost(
2121
&backend.Host{Alias: "host1", Host: "host1.com", User: "user1", Groups: []string{"group1", "group2"}},
2222
false)
@@ -43,7 +43,7 @@ func TestGetAllHosts(t *testing.T) {
4343

4444
func TestGetHostDetails(t *testing.T) {
4545
// test Hosts.GetHostDetails method
46-
cfg := backend.NewTestStorage()
46+
cfg := backend.NewData(&backend.MemoryStorage{})
4747
testHosts := []backend.Host{
4848
{Alias: "host1", Host: "host1.com", User: "user1", Groups: []string{"group1", "group2"}},
4949
}
@@ -71,10 +71,14 @@ func TestGetHostDetails(t *testing.T) {
7171

7272
func TestUpdateHost(t *testing.T) {
7373
// test Hosts.UpdateHost method
74-
cfg := backend.NewTestStorage()
74+
cfg := backend.NewData(&backend.MemoryStorage{})
7575
cfg.AddHost(
7676
&backend.Host{Alias: "host1", Host: "host1.com", User: "user1", Groups: []string{"group1", "group2"}},
7777
false)
78+
cfg.Conn = backend.MockConn(map[string]backend.SFTPMockHost{
79+
"a:22": {Host: "a:22", User: "test", File: "ssh-rsa foo foo\nssh-rsa bar2 bar2\n"},
80+
"b:22": {Host: "b:22", User: "test", File: "ssh-rsa bar1 bar\nssh-rsa bar2 bar2\n"},
81+
})
7882
// mock http request
7983
w := httptest.NewRecorder()
8084
r := httptest.NewRequest(http.MethodPut, "/host1",
@@ -91,7 +95,7 @@ func TestUpdateHost(t *testing.T) {
9195
t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code)
9296
}
9397

94-
var host backend.Host
98+
host := cfg.GetHost("host1")
9599
json.NewDecoder(w.Body).Decode(&host)
96100
if host.User != "user2" {
97101
t.Errorf("Expected user2, got %s %#v", host.User, host)
@@ -100,7 +104,11 @@ func TestUpdateHost(t *testing.T) {
100104

101105
// test Hosts.CreateHost method
102106
func TestCreateHost(t *testing.T) {
103-
cfg := backend.NewTestStorage()
107+
cfg := backend.NewData(&backend.MemoryStorage{})
108+
cfg.Conn = backend.MockConn(map[string]backend.SFTPMockHost{
109+
"a:22": {Host: "a:22", User: "test", File: "ssh-rsa foo foo\nssh-rsa bar2 bar2\n"},
110+
"b:22": {Host: "b:22", User: "test", File: "ssh-rsa bar1 bar\nssh-rsa bar2 bar2\n"},
111+
})
104112
h := HostsHandler{Prefix: ""}
105113
// mock http request
106114
w := httptest.NewRecorder()
@@ -126,7 +134,7 @@ func TestDeleteHost(t *testing.T) {
126134
testHosts := map[string]*backend.Host{
127135
"host1": {Alias: "host1", Host: "host1.com", User: "user1", Groups: []string{"group1", "group2"}},
128136
}
129-
cfg := backend.NewTestStorage()
137+
cfg := backend.NewData(&backend.MemoryStorage{})
130138
cfg.AddHost(testHosts["host1"], false)
131139
h := HostsHandler{Prefix: ""}
132140
// mock http request

api/keys.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ import (
1010
"github.com/shoobyban/sshman/backend"
1111
)
1212

13-
// KeysHandler returns a list of ssh key filename from ~/.ssh
13+
// KeysHandler returns a list of ssh key filenames from ~/.ssh.
1414
type KeysHandler struct {
1515
Prefix string
1616
}
1717

18-
// Config returns a loaded configuration for the handler
19-
func (h KeysHandler) Config(r *http.Request) *backend.Storage {
18+
// Config returns a loaded configuration for the handler.
19+
func (h KeysHandler) Config(r *http.Request) backend.Config {
2020
ctx := r.Context()
21-
if cfg, ok := ctx.Value(ConfigKey).(*backend.Storage); ok {
21+
if cfg, ok := ctx.Value(ConfigKey).(*backend.Data); ok {
2222
return cfg
2323
}
24-
return &backend.Storage{}
24+
return backend.DefaultConfig()
2525
}
2626

27-
// AddRoutes adds keyhandler specific routes to the router
27+
// AddRoutes adds keyhandler specific routes to the router.
2828
func (h KeysHandler) AddRoutes(router *chi.Mux) {
2929
router.Get(h.Prefix, h.GetAllKeys)
3030
// router.Get(h.Prefix+"/{filename}", h.GetKeyDetails)
@@ -33,7 +33,7 @@ func (h KeysHandler) AddRoutes(router *chi.Mux) {
3333
router.Post(h.Prefix, h.CreateKey)
3434
}
3535

36-
// GetAllKeys returns a list of all ssh key files from ~/.ssh filtered by type={all|public|private}
36+
// GetAllKeys returns a list of all ssh key files from ~/.ssh filtered by type={all|public|private}.
3737
func (h KeysHandler) GetAllKeys(w http.ResponseWriter, r *http.Request) {
3838

3939
t := r.URL.Query().Get("type")
@@ -75,7 +75,7 @@ func (h KeysHandler) GetAllKeys(w http.ResponseWriter, r *http.Request) {
7575
e.Encode(keys)
7676
}
7777

78-
// CreateKey creates a new ssh key from uploaded file
78+
// CreateKey creates a new ssh key from an uploaded file.
7979
func (h KeysHandler) CreateKey(w http.ResponseWriter, r *http.Request) {
8080
var payload struct {
8181
Filename string `json:"filename"`

api/logs.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,26 @@ import (
1010
"github.com/shoobyban/sshman/backend"
1111
)
1212

13-
// LogsHandler is a struct for streaming logs to frontend
13+
// LogsHandler is a struct for streaming logs to the frontend.
1414
type LogsHandler struct {
1515
Prefix string
1616
}
1717

18-
// Config returns a loaded configuration for the handler
19-
func (h LogsHandler) Config(r *http.Request) *backend.Storage {
18+
// Config returns a loaded configuration for the handler.
19+
func (h LogsHandler) Config(r *http.Request) backend.Config {
2020
ctx := r.Context()
21-
if cfg, ok := ctx.Value(ConfigKey).(*backend.Storage); ok {
21+
if cfg, ok := ctx.Value(ConfigKey).(*backend.Data); ok {
2222
return cfg
2323
}
24-
return &backend.Storage{}
24+
return backend.DefaultConfig()
2525
}
2626

27-
// AddRoutes adds logs handler specific routes to the router
27+
// AddRoutes adds logs handler specific routes to the router.
2828
func (h LogsHandler) AddRoutes(router *chi.Mux) {
2929
router.Get(h.Prefix, h.GetLogs)
3030
}
3131

32-
// GetLogs streams logs on a keep-alive connection to the frontend
32+
// GetLogs streams logs on a keep-alive connection to the frontend.
3333
func (h LogsHandler) GetLogs(w http.ResponseWriter, r *http.Request) {
3434
w.Header().Set("Content-Type", "text/event-stream")
3535
w.Header().Set("Cache-Control", "no-cache")

api/users.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ import (
1010
"github.com/shoobyban/sshman/backend"
1111
)
1212

13-
// UsersHandler is the handler for users
13+
// UsersHandler is the handler for user-related operations
1414
type UsersHandler struct {
1515
Prefix string
1616
}
1717

1818
// Config returns the config for the handler
1919
func (h UsersHandler) Config(r *http.Request) backend.Config {
2020
ctx := r.Context()
21-
if cfg, ok := ctx.Value(ConfigKey).(*backend.Storage); ok {
21+
if cfg, ok := ctx.Value(ConfigKey).(*backend.Data); ok {
2222
return cfg
2323
}
24-
return &backend.Storage{}
24+
return backend.DefaultConfig()
2525
}
2626

2727
// AddRoutes adds the routes for the handler
@@ -151,7 +151,7 @@ func (h UsersHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
151151
json.NewEncoder(w).Encode(user)
152152
}
153153

154-
// DeleteUser deletes a user
154+
// DeleteUser deletes a user.
155155
func (h UsersHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
156156
id := chi.URLParam(r, "id")
157157
log.Printf("Deleting user %s", id)

0 commit comments

Comments
 (0)