From 2eb5444a4e74536d5ce50cb48c117b1c4cd4091f Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Tue, 13 Nov 2018 11:28:21 -0800 Subject: [PATCH] form use api routes - api updates --- Makefile | 2 +- cmd/main_test.go | 3 +- core/users.go | 7 ++ handlers/api.go | 174 ++++++++------------------------ handlers/handlers_test.go | 180 ++-------------------------------- handlers/route_api_test.go | 6 +- handlers/routes.go | 12 +-- handlers/users.go | 155 +++++++++++++++++------------ notifiers/webhook_test.go | 4 +- source/css/base.css | 3 + source/js/main.js | 132 ++++++++++++++++++++++++- source/scss/base.scss | 4 + source/source.go | 2 +- source/tmpl/dashboard.html | 12 ++- source/tmpl/form_message.html | 6 +- source/tmpl/form_service.html | 6 +- source/tmpl/form_user.html | 13 ++- source/tmpl/help.md | 3 - source/tmpl/messages.html | 6 +- source/tmpl/services.html | 9 +- source/tmpl/settings.html | 7 +- source/tmpl/users.html | 8 +- types/user.go | 10 +- 23 files changed, 350 insertions(+), 414 deletions(-) diff --git a/Makefile b/Makefile index 5fd1f114..3589077b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION=0.79.6 +VERSION=0.79.7 BINARY_NAME=statup GOPATH:=$(GOPATH) GOCMD=go diff --git a/cmd/main_test.go b/cmd/main_test.go index 07746929..c14952b3 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -110,7 +110,6 @@ func TestRunAll(t *testing.T) { }) t.Run(dbt+" Select Core", func(t *testing.T) { RunSelectCoreMYQL(t, dbt) - t.Log(core.CoreApp) }) t.Run(dbt+" Select Services", func(t *testing.T) { RunSelectAllMysqlServices(t) @@ -556,6 +555,7 @@ func RunUsersHandler(t *testing.T) { assert.Nil(t, err) rr := httptest.NewRecorder() route.ServeHTTP(rr, req) + t.Log(rr.Body.String()) assert.True(t, strings.Contains(rr.Body.String(), "Statup | Users")) assert.True(t, strings.Contains(rr.Body.String(), "footer")) assert.True(t, handlers.IsAuthenticated(req)) @@ -566,6 +566,7 @@ func RunUserViewHandler(t *testing.T) { assert.Nil(t, err) rr := httptest.NewRecorder() route.ServeHTTP(rr, req) + t.Log(rr.Body.String()) assert.True(t, strings.Contains(rr.Body.String(), "Statup | testadmin")) assert.True(t, strings.Contains(rr.Body.String(), "footer")) assert.True(t, handlers.IsAuthenticated(req)) diff --git a/core/users.go b/core/users.go index f87d9eaa..a22965b0 100644 --- a/core/users.go +++ b/core/users.go @@ -32,6 +32,13 @@ func ReturnUser(u *types.User) *user { return &user{u} } +// CountUsers returns the amount of users +func CountUsers() int64 { + var amount int64 + usersDB().Count(&amount) + return amount +} + // SelectUser returns the user based on the user's ID. func SelectUser(id int64) (*user, error) { var user user diff --git a/handlers/api.go b/handlers/api.go index c3808af7..2b529cf1 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -17,6 +17,7 @@ package handlers import ( "encoding/json" + "errors" "fmt" "github.com/gorilla/mux" "github.com/hunterlong/statup/core" @@ -30,9 +31,10 @@ import ( type apiResponse struct { Status string `json:"status"` - Object string `json:"type"` - Id int64 `json:"id"` - Method string `json:"method"` + Object string `json:"type,omitempty"` + Id int64 `json:"id,omitempty"` + Method string `json:"method,omitempty"` + Error string `json:"error,omitempty"` } func apiIndexHandler(w http.ResponseWriter, r *http.Request) { @@ -63,7 +65,8 @@ func apiRenewHandler(w http.ResponseWriter, r *http.Request) { core.CoreApp.ApiSecret = utils.NewSHA1Hash(40) core.CoreApp, err = core.UpdateCore(core.CoreApp) if err != nil { - utils.Log(3, err) + sendErrorJson(err, w, r) + return } http.Redirect(w, r, "/settings", http.StatusSeeOther) } @@ -84,7 +87,7 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) service := core.SelectService(utils.StringInt(vars["id"])) if service == nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + sendErrorJson(errors.New("service data not found"), w, r) return } fields := parseGet(r) @@ -106,7 +109,7 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) service := core.SelectService(utils.StringInt(vars["id"])) if service == nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + sendErrorJson(errors.New("service not found"), w, r) return } fields := parseGet(r) @@ -127,7 +130,7 @@ func apiServiceHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) service := core.SelectServicer(utils.StringInt(vars["id"])) if service == nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + sendErrorJson(errors.New("service not found"), w, r) return } @@ -144,13 +147,13 @@ func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) err := decoder.Decode(&service) if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } newService := core.ReturnService(service) _, err = newService.Create(true) if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } w.Header().Set("Content-Type", "application/json") @@ -165,7 +168,7 @@ func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) service := core.SelectService(utils.StringInt(vars["id"])) if service == nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + sendErrorJson(errors.New("service not found"), w, r) return } var updatedService *types.Service @@ -175,7 +178,7 @@ func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) { service = core.ReturnService(updatedService) err := service.Update(true) if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } service.Check(true) @@ -191,12 +194,12 @@ func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) service := core.SelectService(utils.StringInt(vars["id"])) if service == nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + sendErrorJson(errors.New("service not found"), w, r) return } err := service.Delete() if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } output := apiResponse{ @@ -223,113 +226,6 @@ func apiAllServicesHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(servicesOut) } -func apiUserHandler(w http.ResponseWriter, r *http.Request) { - if !isAPIAuthorized(r) { - sendUnauthorizedJson(w, r) - return - } - vars := mux.Vars(r) - user, err := core.SelectUser(utils.StringInt(vars["id"])) - if err != nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(user) -} - -func apiUserUpdateHandler(w http.ResponseWriter, r *http.Request) { - if !isAPIAuthorized(r) { - sendUnauthorizedJson(w, r) - return - } - vars := mux.Vars(r) - user, err := core.SelectUser(utils.StringInt(vars["id"])) - if err != nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return - } - var updateUser *types.User - decoder := json.NewDecoder(r.Body) - decoder.Decode(&updateUser) - updateUser.Id = user.Id - user = core.ReturnUser(updateUser) - err = user.Update() - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(user) -} - -func apiUserDeleteHandler(w http.ResponseWriter, r *http.Request) { - if !isAPIAuthorized(r) { - sendUnauthorizedJson(w, r) - return - } - vars := mux.Vars(r) - user, err := core.SelectUser(utils.StringInt(vars["id"])) - if err != nil { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return - } - err = user.Delete() - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - output := apiResponse{ - Object: "user", - Method: "delete", - Id: user.Id, - Status: "success", - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(output) -} - -func apiAllUsersHandler(w http.ResponseWriter, r *http.Request) { - if !isAPIAuthorized(r) { - sendUnauthorizedJson(w, r) - return - } - users, err := core.SelectAllUsers() - if err != nil { - utils.Log(3, err) - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(users) -} - -func apiCreateUsersHandler(w http.ResponseWriter, r *http.Request) { - if !isAPIAuthorized(r) { - sendUnauthorizedJson(w, r) - return - } - var user *types.User - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&user) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - newUser := core.ReturnUser(user) - uId, err := newUser.Create() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - output := apiResponse{ - Object: "user", - Method: "create", - Id: uId, - Status: "success", - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(output) -} - func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) { if !isAPIAuthorized(r) { sendUnauthorizedJson(w, r) @@ -338,7 +234,7 @@ func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) _, notifierObj, err := notifier.SelectNotifier(vars["notifier"]) if err != nil { - http.Error(w, fmt.Sprintf("%v notifier was not found", vars["notifier"]), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } w.Header().Set("Content-Type", "application/json") @@ -358,7 +254,7 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) { notifer, not, err := notifier.SelectNotifier(vars["notifier"]) if err != nil { - http.Error(w, fmt.Sprintf("%v notifier was not found", vars["notifier"]), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } @@ -374,7 +270,8 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) { _, err = notifier.Update(not, notifer) if err != nil { - utils.Log(3, fmt.Sprintf("issue updating notifier: %v", err)) + sendErrorJson(err, w, r) + return } notifier.OnSave(notifer.Method) @@ -389,7 +286,7 @@ func apiAllMessagesHandler(w http.ResponseWriter, r *http.Request) { } messages, err := core.SelectMessages() if err != nil { - http.Error(w, fmt.Sprintf("error fetching all messages: %v", err), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } w.Header().Set("Content-Type", "application/json") @@ -404,7 +301,7 @@ func apiMessageGetHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) message, err := core.SelectMessage(utils.StringInt(vars["id"])) if err != nil { - http.Error(w, fmt.Sprintf("message #%v was not found", vars["id"]), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } w.Header().Set("Content-Type", "application/json") @@ -419,12 +316,12 @@ func apiMessageDeleteHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) message, err := core.SelectMessage(utils.StringInt(vars["id"])) if err != nil { - http.Error(w, fmt.Sprintf("message #%v was not found", vars["id"]), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } err = message.Delete() if err != nil { - http.Error(w, fmt.Sprintf("message #%v could not be deleted %v", vars["id"], err), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } @@ -447,14 +344,14 @@ func apiMessageUpdateHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) message, err := core.SelectMessage(utils.StringInt(vars["id"])) if err != nil { - http.Error(w, fmt.Sprintf("message #%v was not found", vars["id"]), http.StatusInternalServerError) + sendErrorJson(fmt.Errorf("message #%v was not found", vars["id"]), w, r) return } var messageBody *types.Message decoder := json.NewDecoder(r.Body) err = decoder.Decode(&messageBody) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } @@ -462,7 +359,7 @@ func apiMessageUpdateHandler(w http.ResponseWriter, r *http.Request) { message = core.ReturnMessage(messageBody) _, err = message.Update() if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + sendErrorJson(err, w, r) return } @@ -512,14 +409,23 @@ func apiServiceFailuresHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(service.AllFailures()) } +func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) { + output := apiResponse{ + Status: "error", + Error: err.Error(), + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(output) +} + func sendUnauthorizedJson(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "error": "unauthorized", - "url": r.RequestURI, + output := apiResponse{ + Status: "error", + Error: errors.New("not authorized").Error(), } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(data) + json.NewEncoder(w).Encode(output) } func isAPIAuthorized(r *http.Request) bool { diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index ccbdf654..dc66ca94 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -16,7 +16,6 @@ package handlers import ( - "fmt" "github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core/notifier" _ "github.com/hunterlong/statup/notifiers" @@ -175,53 +174,6 @@ func TestServicesHandler(t *testing.T) { assert.True(t, isRouteAuthenticated(req)) } -func TestCreateUserHandler(t *testing.T) { - form := url.Values{} - form.Add("username", "newuser") - form.Add("password", "password123") - form.Add("email", "info@okokk.com") - form.Add("admin", "on") - req, err := http.NewRequest("POST", "/users", strings.NewReader(form.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - assert.Nil(t, err) - rr := httptest.NewRecorder() - Router().ServeHTTP(rr, req) - assert.Equal(t, 303, rr.Code) - assert.True(t, isRouteAuthenticated(req)) -} - -func TestEditUserHandler(t *testing.T) { - form := url.Values{} - form.Add("username", "changedusername") - form.Add("password", "##########") - form.Add("email", "info@okokk.com") - form.Add("admin", "on") - req, err := http.NewRequest("POST", "/user/2", strings.NewReader(form.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - assert.Nil(t, err) - rr := httptest.NewRecorder() - Router().ServeHTTP(rr, req) - - req, err = http.NewRequest("GET", "/users", nil) - assert.Nil(t, err) - rr = httptest.NewRecorder() - Router().ServeHTTP(rr, req) - body := rr.Body.String() - assert.Contains(t, body, "admin") - assert.Contains(t, body, "changedusername") - assert.Equal(t, 200, rr.Code) - assert.True(t, isRouteAuthenticated(req)) -} - -func TestDeleteUserHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/user/2/delete", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - Router().ServeHTTP(rr, req) - assert.Equal(t, 303, rr.Code) - assert.True(t, isRouteAuthenticated(req)) -} - func TestUsersHandler(t *testing.T) { req, err := http.NewRequest("GET", "/users", nil) assert.Nil(t, err) @@ -275,48 +227,6 @@ func TestHelpHandler(t *testing.T) { assert.True(t, isRouteAuthenticated(req)) } -func TestCreateHTTPServiceHandler(t *testing.T) { - form := url.Values{} - form.Add("name", "Crystal Castles - Kept") - form.Add("domain", "https://www.youtube.com/watch?v=CfbCLwNlGwU") - form.Add("method", "GET") - form.Add("expected_status", "200") - form.Add("interval", "30") - form.Add("port", "") - form.Add("timeout", "30") - form.Add("check_type", "http") - form.Add("post_data", "") - - req, err := http.NewRequest("POST", "/services", strings.NewReader(form.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - assert.Nil(t, err) - rr := httptest.NewRecorder() - Router().ServeHTTP(rr, req) - assert.Equal(t, 303, rr.Code) - assert.True(t, isRouteAuthenticated(req)) -} - -func TestCreateTCPerviceHandler(t *testing.T) { - form := url.Values{} - form.Add("name", "Local Postgres") - form.Add("domain", "localhost") - form.Add("method", "GET") - form.Add("expected_status", "") - form.Add("interval", "30") - form.Add("port", "5432") - form.Add("timeout", "30") - form.Add("check_type", "tcp") - form.Add("post_data", "") - - req, err := http.NewRequest("POST", "/services", strings.NewReader(form.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - assert.Nil(t, err) - rr := httptest.NewRecorder() - Router().ServeHTTP(rr, req) - assert.Equal(t, 303, rr.Code) - assert.True(t, isRouteAuthenticated(req)) -} - func TestServicesHandler2(t *testing.T) { req, err := http.NewRequest("GET", "/services", nil) assert.Nil(t, err) @@ -325,36 +235,36 @@ func TestServicesHandler2(t *testing.T) { body := rr.Body.String() assert.Equal(t, 200, rr.Code) assert.Contains(t, body, "Statup | Services") - assert.Contains(t, body, "Crystal Castles - Kept") - assert.Contains(t, body, "Local Postgres") + assert.Contains(t, body, "JSON Users Test") + assert.Contains(t, body, "JSON API Tester") //assert.Contains(t, body, "️") assert.True(t, isRouteAuthenticated(req)) } func TestViewHTTPServicesHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/service/6", nil) + req, err := http.NewRequest("GET", "/service/5", nil) assert.Nil(t, err) rr := httptest.NewRecorder() Router().ServeHTTP(rr, req) body := rr.Body.String() assert.Equal(t, 200, rr.Code) - assert.Contains(t, body, "Crystal Castles - Kept Status") + assert.Contains(t, body, "Google DNS Status") //assert.Contains(t, body, "️") } func TestViewTCPServicesHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/service/7", nil) + req, err := http.NewRequest("GET", "/service/5", nil) assert.Nil(t, err) rr := httptest.NewRecorder() Router().ServeHTTP(rr, req) body := rr.Body.String() assert.Equal(t, 200, rr.Code) - assert.Contains(t, body, "Local Postgres Status") + assert.Contains(t, body, "Google DNS Status") //assert.Contains(t, body, "️") } func TestServicesDeleteFailuresHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/service/7/delete_failures", nil) + req, err := http.NewRequest("GET", "/service/5/delete_failures", nil) assert.Nil(t, err) rr := httptest.NewRecorder() Router().ServeHTTP(rr, req) @@ -363,43 +273,7 @@ func TestServicesDeleteFailuresHandler(t *testing.T) { } func TestFailingServicesDeleteFailuresHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/service/1/delete_failures", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - Router().ServeHTTP(rr, req) - assert.Equal(t, 303, rr.Code) - assert.True(t, isRouteAuthenticated(req)) -} - -func TestServicesUpdateHandler(t *testing.T) { - form := url.Values{} - form.Add("name", "The Bravery - An Honest Mistake") - form.Add("domain", "https://www.youtube.com/watch?v=O8vzbezVru4") - form.Add("method", "GET") - form.Add("expected_status", "") - form.Add("interval", "30") - form.Add("port", "") - form.Add("timeout", "15") - form.Add("check_type", "http") - form.Add("post_data", "") - req, err := http.NewRequest("POST", "/service/6", strings.NewReader(form.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - assert.Nil(t, err) - rr := httptest.NewRecorder() - Router().ServeHTTP(rr, req) - - req, err = http.NewRequest("GET", "/service/6", nil) - assert.Nil(t, err) - rr = httptest.NewRecorder() - Router().ServeHTTP(rr, req) - body := rr.Body.String() - assert.Equal(t, 200, rr.Code) - assert.Contains(t, body, "The Bravery - An Honest Mistake Status") - //assert.Contains(t, body, "️") -} - -func TestDeleteServiceHandler(t *testing.T) { - req, err := http.NewRequest("POST", "/service/7/delete", nil) + req, err := http.NewRequest("GET", "/service/5/delete_failures", nil) assert.Nil(t, err) rr := httptest.NewRecorder() Router().ServeHTTP(rr, req) @@ -486,7 +360,7 @@ func TestPrometheusHandler(t *testing.T) { Router().ServeHTTP(rr, req) body := rr.Body.String() assert.Equal(t, 200, rr.Code) - assert.Contains(t, body, "statup_total_services 6") + assert.Contains(t, body, "statup_total_services 5") assert.True(t, isRouteAuthenticated(req)) } @@ -622,42 +496,6 @@ func TestExportHandler(t *testing.T) { assert.True(t, isRouteAuthenticated(req)) } -func TestCreateBulkServices(t *testing.T) { - domains := []string{ - "https://status.coinapp.io", - "https://demo.statup.io", - "https://golang.org", - "https://github.com/hunterlong", - "https://www.santamonica.com", - "https://www.oeschs-die-dritten.ch/en/", - "https://etherscan.io", - "https://www.youtube.com/watch?v=ipvEIZMMILA", - "https://www.youtube.com/watch?v=UdaYVxYF1Ok", - "https://www.youtube.com/watch?v=yydZbVoCbn0&t=870s", - "http://failingdomainsarenofunatall.com", - } - for k, d := range domains { - form := url.Values{} - form.Add("name", fmt.Sprintf("Test Service %v", k)) - form.Add("domain", d) - form.Add("method", "GET") - form.Add("expected_status", "200") - form.Add("interval", fmt.Sprintf("%v", k+1)) - form.Add("port", "") - form.Add("timeout", "30") - form.Add("check_type", "http") - form.Add("post_data", "") - - req, err := http.NewRequest("POST", "/services", strings.NewReader(form.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - assert.Nil(t, err) - rr := httptest.NewRecorder() - Router().ServeHTTP(rr, req) - assert.Equal(t, 303, rr.Code) - assert.True(t, isRouteAuthenticated(req)) - } -} - func isRouteAuthenticated(req *http.Request) bool { os.Setenv("GO_ENV", "production") rr := httptest.NewRecorder() diff --git a/handlers/route_api_test.go b/handlers/route_api_test.go index ab73cd4d..6aeea75c 100644 --- a/handlers/route_api_test.go +++ b/handlers/route_api_test.go @@ -107,8 +107,8 @@ func TestApiAllServicesHandlerHandler(t *testing.T) { var obj []types.Service formatJSON(body, &obj) assert.Equal(t, 200, rr.Code) - assert.Equal(t, "Test Service 9", obj[0].Name) - assert.Equal(t, "https://www.youtube.com/watch?v=yydZbVoCbn0&t=870s", obj[0].Domain) + assert.Equal(t, "Google", obj[0].Name) + assert.Equal(t, "https://google.com", obj[0].Domain) } func TestApiServiceHandler(t *testing.T) { @@ -212,7 +212,6 @@ func TestApiViewUserHandler(t *testing.T) { func TestApiUpdateUserHandler(t *testing.T) { data := `{ "username": "adminupdated", - "email": "info@email.com", "password": "password123", "admin": true}` rr, err := httpRequestAPI(t, "POST", "/api/users/1", strings.NewReader(data)) @@ -220,6 +219,7 @@ func TestApiUpdateUserHandler(t *testing.T) { body := rr.Body.String() var obj types.User formatJSON(body, &obj) + t.Log(body) assert.Equal(t, 200, rr.Code) assert.Equal(t, "adminupdated", obj.Username) assert.Equal(t, true, obj.Admin.Bool) diff --git a/handlers/routes.go b/handlers/routes.go index 5492857b..3996c6fa 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -65,17 +65,10 @@ func Router() *mux.Router { // USER Routes r.Handle("/users", http.HandlerFunc(usersHandler)).Methods("GET") - r.Handle("/users", http.HandlerFunc(createUserHandler)).Methods("POST") r.Handle("/user/{id}", http.HandlerFunc(usersEditHandler)).Methods("GET") - r.Handle("/user/{id}", http.HandlerFunc(updateUserHandler)).Methods("POST") - r.Handle("/user/{id}/delete", http.HandlerFunc(usersDeleteHandler)).Methods("GET") // MESSAGES Routes r.Handle("/messages", http.HandlerFunc(messagesHandler)).Methods("GET") - r.Handle("/messages", http.HandlerFunc(createMessageHandler)).Methods("POST") - r.Handle("/message/{id}", http.HandlerFunc(viewMessageHandler)).Methods("GET") - r.Handle("/message/{id}", http.HandlerFunc(updateMessageHandler)).Methods("POST") - r.Handle("/message/{id}/delete", http.HandlerFunc(deleteMessageHandler)).Methods("GET") // SETTINGS Routes r.Handle("/settings", http.HandlerFunc(settingsHandler)).Methods("GET") @@ -89,14 +82,13 @@ func Router() *mux.Router { // SERVICE Routes r.Handle("/services", http.HandlerFunc(servicesHandler)).Methods("GET") - r.Handle("/services", http.HandlerFunc(createServiceHandler)).Methods("POST") r.Handle("/services/reorder", http.HandlerFunc(reorderServiceHandler)).Methods("POST") r.Handle("/service/{id}", http.HandlerFunc(servicesViewHandler)).Methods("GET") - r.Handle("/service/{id}", http.HandlerFunc(servicesUpdateHandler)).Methods("POST") r.Handle("/service/{id}/edit", http.HandlerFunc(servicesViewHandler)) - r.Handle("/service/{id}/delete", http.HandlerFunc(servicesDeleteHandler)) r.Handle("/service/{id}/delete_failures", http.HandlerFunc(servicesDeleteFailuresHandler)).Methods("GET") r.Handle("/service/{id}/checkin", http.HandlerFunc(checkinCreateHandler)).Methods("POST") + + // CHECKIN Routes r.Handle("/checkin/{id}/delete", http.HandlerFunc(checkinDeleteHandler)).Methods("GET") r.Handle("/checkin/{id}", http.HandlerFunc(checkinHitHandler)) diff --git a/handlers/users.go b/handlers/users.go index 3eeb0690..c1b4d9e8 100644 --- a/handlers/users.go +++ b/handlers/users.go @@ -16,7 +16,8 @@ package handlers import ( - "fmt" + "encoding/json" + "errors" "github.com/gorilla/mux" "github.com/hunterlong/statup/core" "github.com/hunterlong/statup/types" @@ -45,82 +46,116 @@ func usersEditHandler(w http.ResponseWriter, r *http.Request) { executeResponse(w, r, "user.html", user, nil) } -func updateUserHandler(w http.ResponseWriter, r *http.Request) { - if !IsAuthenticated(r) { - http.Redirect(w, r, "/", http.StatusSeeOther) +func apiUserHandler(w http.ResponseWriter, r *http.Request) { + if !isAPIAuthorized(r) { + sendUnauthorizedJson(w, r) return } - r.ParseForm() vars := mux.Vars(r) - id := utils.StringInt(vars["id"]) - user, err := core.SelectUser(id) + user, err := core.SelectUser(utils.StringInt(vars["id"])) if err != nil { - utils.Log(3, fmt.Sprintf("user error: %v", err)) - w.WriteHeader(http.StatusInternalServerError) + sendErrorJson(err, w, r) return } + user.Password = "" + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) +} - user.Username = r.PostForm.Get("username") - user.Email = r.PostForm.Get("email") - isAdmin := r.PostForm.Get("admin") == "on" - user.Admin = types.NewNullBool(isAdmin) - password := r.PostForm.Get("password") - if password != "##########" { - user.Password = utils.HashPassword(password) +func apiUserUpdateHandler(w http.ResponseWriter, r *http.Request) { + if !isAPIAuthorized(r) { + sendUnauthorizedJson(w, r) + return } + vars := mux.Vars(r) + user, err := core.SelectUser(utils.StringInt(vars["id"])) + if err != nil { + sendErrorJson(err, w, r) + return + } + var updateUser *types.User + decoder := json.NewDecoder(r.Body) + decoder.Decode(&updateUser) + updateUser.Id = user.Id + user = core.ReturnUser(updateUser) err = user.Update() if err != nil { - utils.Log(3, err) - } - - fmt.Println(user.Id) - fmt.Println(user.Password) - fmt.Println(user.Admin.Bool) - - users, _ := core.SelectAllUsers() - executeResponse(w, r, "users.html", users, "/users") -} - -func createUserHandler(w http.ResponseWriter, r *http.Request) { - if !IsAuthenticated(r) { - http.Redirect(w, r, "/", http.StatusSeeOther) + sendErrorJson(err, w, r) return } - r.ParseForm() - username := r.PostForm.Get("username") - password := r.PostForm.Get("password") - email := r.PostForm.Get("email") - admin := r.PostForm.Get("admin") - - user := core.ReturnUser(&types.User{ - Username: username, - Password: password, - Email: email, - Admin: types.NewNullBool(admin == "on"), - }) - _, err := user.Create() - if err != nil { - utils.Log(3, err) - } - //notifiers.OnNewUser(user) - executeResponse(w, r, "users.html", user, "/users") + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) } -func usersDeleteHandler(w http.ResponseWriter, r *http.Request) { - if !IsAuthenticated(r) { - http.Redirect(w, r, "/", http.StatusSeeOther) +func apiUserDeleteHandler(w http.ResponseWriter, r *http.Request) { + if !isAPIAuthorized(r) { + sendUnauthorizedJson(w, r) return } vars := mux.Vars(r) - id, _ := strconv.Atoi(vars["id"]) - user, _ := core.SelectUser(int64(id)) - - users, _ := core.SelectAllUsers() - if len(users) == 1 { - utils.Log(2, "cannot delete the only user in the system") - http.Redirect(w, r, "/users", http.StatusSeeOther) + users := core.CountUsers() + if users == 1 { + sendErrorJson(errors.New("cannot delete the last user"), w, r) return } - user.Delete() - http.Redirect(w, r, "/users", http.StatusSeeOther) + user, err := core.SelectUser(utils.StringInt(vars["id"])) + if err != nil { + sendErrorJson(err, w, r) + return + } + err = user.Delete() + if err != nil { + sendErrorJson(err, w, r) + return + } + output := apiResponse{ + Object: "user", + Method: "delete", + Id: user.Id, + Status: "success", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(output) +} + +func apiAllUsersHandler(w http.ResponseWriter, r *http.Request) { + if !isAPIAuthorized(r) { + sendUnauthorizedJson(w, r) + return + } + users, err := core.SelectAllUsers() + if err != nil { + sendErrorJson(err, w, r) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(users) +} + +func apiCreateUsersHandler(w http.ResponseWriter, r *http.Request) { + if !isAPIAuthorized(r) { + sendUnauthorizedJson(w, r) + return + } + var user *types.User + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&user) + if err != nil { + sendErrorJson(err, w, r) + return + } + newUser := core.ReturnUser(user) + uId, err := newUser.Create() + if err != nil { + sendErrorJson(err, w, r) + return + } + output := apiResponse{ + Object: "user", + Method: "create", + Id: uId, + Status: "success", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(output) } diff --git a/notifiers/webhook_test.go b/notifiers/webhook_test.go index a4b3d1d5..7bf7558c 100644 --- a/notifiers/webhook_test.go +++ b/notifiers/webhook_test.go @@ -23,7 +23,7 @@ import ( ) var ( - webhookTestUrl = "http://localhost:5555" + webhookTestUrl = "https://demo.statup.com/api/services" webhookMessage = `{"id": "%service.Id","name": "%service.Name","online": "%service.Online","issue": "%failure.Issue"}` apiKey = "application/json" fullMsg string @@ -99,7 +99,7 @@ func TestWebhookNotifier(t *testing.T) { t.Run("webhooker Send", func(t *testing.T) { err := webhook.Send(fullMsg) - assert.Nil(t, err) + assert.Error(t, err) assert.Equal(t, len(webhook.Queue), 1) }) diff --git a/source/css/base.css b/source/css/base.css index 5bdd6b4b..45a4d063 100644 --- a/source/css/base.css +++ b/source/css/base.css @@ -405,6 +405,9 @@ HTML, BODY { .pointer { cursor: pointer; } +.jumbotron { + background-color: white; } + @media (max-width: 767px) { HTML, BODY { background-color: #fcfcfc; } diff --git a/source/js/main.js b/source/js/main.js index dd65246f..c6b771c7 100644 --- a/source/js/main.js +++ b/source/js/main.js @@ -59,11 +59,23 @@ $('.test_notifier').on('click', function(e) { }); $('form').submit(function() { - var spinner = ''; - $(this).find('button[type=submit]').prop('disabled', true); - $(this).find('button[type=submit]').html(spinner); + // Spinner($(this).find('button[type=submit]')) }); + +function Spinner(btn, off = false) { + btn.prop('disabled', !off); + if (off) { + let pastVal = btn.attr("data-past"); + btn.text(pastVal); + btn.removeAttr("data-past"); + } else { + let pastVal = btn.text(); + btn.attr("data-past", pastVal); + btn.html(''); + } +} + $('select#service_type').on('change', function() { var selected = $('#service_type option:selected').val(); var typeLabel = $('#service_type_label'); @@ -122,6 +134,120 @@ function PingAjaxChart(chart, service, start=0, end=9999999999, group="hour") { }); } +$('.ajax_delete').on('click', function() { + let obj = $(this); + let id = obj.attr('data-id'); + let element = obj.attr('data-obj'); + let url = obj.attr('href'); + let method = obj.attr('data-method'); + $.ajax({ + url: url, + type: method, + data: JSON.stringify({id: id}), + success: function (data) { + if (data.status === 'error') { + alert(data.error) + } else { + console.log(data); + $('#' + element).remove(); + } + } + }); + return false +}); + + +$('form.ajax_form').on('submit', function() { + const form = $(this); + let values = form.serializeArray(); + let method = form.attr('method'); + let action = form.attr('action'); + let func = form.attr('data-func'); + let redirect = form.attr('data-redirect'); + let button = form.find('button[type=submit]'); + let alerter = form.find('#alerter'); + var arrayData = []; + let newArr = {}; + Spinner(button); + values.forEach(function(k, v) { + if (k.name === "password_confirm") { + return + } + if (k.value === "") { + return + } + if (k.value === "on") { + k.value = true + } + if($.isNumeric(k.value)){ + if (k.name !== "password") { + k.value = parseInt(k.value) + } + } + newArr[k.name] = k.value; + arrayData.push(newArr) + }); + let sendData = JSON.stringify(newArr); + console.log('sending '+method.toUpperCase()+' '+action+':', newArr); + $.ajax({ + url: action, + type: method, + data: sendData, + success: function (data) { + console.log(data) + if (data.status === 'error') { + let alerter = form.find('#alerter'); + alerter.html(data.error); + alerter.removeClass("d-none"); + Spinner(button, true); + } else { + Spinner(button, true); + if (func) { + let fn = window[func]; + if (typeof fn === "function") fn({form: newArr, data: data}); + } + if (redirect) { + window.location.href = redirect; + } + } + } + }); + return false; +}); + +function CreateService(output) { + console.log('creating service', output) + let form = output.form; + let data = output.data; + let objTbl = ` + ${form.name} + ${data.online}ONLINE + +
+ View + +
+ + `; + $('#services_table').append(objTbl); +} + +function CreateUser(output) { + console.log('creating user', output) + let form = output.form; + let data = output.data; + let objTbl = ` + ${form.username} + +
+ Edit + +
+ + `; + $('#users_table').append(objTbl); +} + $('select#service_check_type').on('change', function() { var selected = $('#service_check_type option:selected').val(); if (selected === 'POST') { diff --git a/source/scss/base.scss b/source/scss/base.scss index 9e5ffbd1..92f977c7 100644 --- a/source/scss/base.scss +++ b/source/scss/base.scss @@ -468,4 +468,8 @@ HTML,BODY { cursor: pointer; } +.jumbotron { + background-color: white; +} + @import 'mobile'; diff --git a/source/source.go b/source/source.go index 092bcc60..82a8bbc1 100644 --- a/source/source.go +++ b/source/source.go @@ -199,7 +199,7 @@ func CopyAllToPublic(box *rice.Box, folder string) error { if err != nil { return nil } - filePath := fmt.Sprintf("%v%v", folder, path) + filePath := fmt.Sprintf("%v/%v", folder, path) SaveAsset(file, utils.Directory, filePath) return nil }) diff --git a/source/tmpl/dashboard.html b/source/tmpl/dashboard.html index c7a7e9bf..65a7c789 100644 --- a/source/tmpl/dashboard.html +++ b/source/tmpl/dashboard.html @@ -19,7 +19,14 @@
-

Services

+ {{if eq (len CoreApp.Services) 0}} + + {{else}}
{{ range Services }} @@ -29,7 +36,8 @@

{{.SmallText}}

- {{ end }} + {{ end }} + {{end}}
{{ range Services }} diff --git a/source/tmpl/form_message.html b/source/tmpl/form_message.html index 4b637c89..6292eb95 100644 --- a/source/tmpl/form_message.html +++ b/source/tmpl/form_message.html @@ -1,6 +1,10 @@ {{define "form_message"}} {{$message := .}} -
+{{if ne .Id 0}} + +{{else}} + +{{end}}
diff --git a/source/tmpl/form_service.html b/source/tmpl/form_service.html index 52b8247d..2715274b 100644 --- a/source/tmpl/form_service.html +++ b/source/tmpl/form_service.html @@ -1,5 +1,9 @@ {{define "form_service"}} - +{{if ne .Id 0}} + +{{else}} + +{{end}}
diff --git a/source/tmpl/form_user.html b/source/tmpl/form_user.html index f49e68ad..b3ed2064 100644 --- a/source/tmpl/form_user.html +++ b/source/tmpl/form_user.html @@ -1,5 +1,9 @@ {{define "form_user"}} - +{{if ne .Id 0}} + +{{else}} + +{{end}}
@@ -21,13 +25,13 @@
- +
- +
@@ -35,5 +39,6 @@
+ -{{end}} +{{end}} \ No newline at end of file diff --git a/source/tmpl/help.md b/source/tmpl/help.md index d0021258..5e09043b 100644 --- a/source/tmpl/help.md +++ b/source/tmpl/help.md @@ -17,9 +17,6 @@ You can change multiple settings in your Statup instance. # Users Users can access the Statup Dashboard to add, remove, and view services. -# Notifications - - # Plugins Creating a plugin for Statup is not that difficult, if you know a little bit of Go Language you can create any type of application to be embedded into the Status framework. Checkout the example plugin that includes all the interfaces, information, and custom HTTP routing at https://github.com/hunterlong/statup_plugin. diff --git a/source/tmpl/messages.html b/source/tmpl/messages.html index aa4f79b4..8e48ce00 100644 --- a/source/tmpl/messages.html +++ b/source/tmpl/messages.html @@ -16,14 +16,14 @@ {{range .}} - + {{.Title}} {{if .Service}}{{.Service.Name}}{{end}} {{Duration 0}} - +
Edit - +
diff --git a/source/tmpl/services.html b/source/tmpl/services.html index 62a62484..cc9a19e0 100644 --- a/source/tmpl/services.html +++ b/source/tmpl/services.html @@ -5,8 +5,8 @@
+ {{if ne (len .) 0}}

Services

- @@ -15,21 +15,22 @@ - + {{range .}} - + {{end}}
{{.Name}} {{if .Online}}ONLINE{{else}}OFFLINE{{end}}
View - +
+ {{end}}

Create Service

diff --git a/source/tmpl/settings.html b/source/tmpl/settings.html index a3d19f57..f731a129 100644 --- a/source/tmpl/settings.html +++ b/source/tmpl/settings.html @@ -127,7 +127,12 @@
{{if not .UsingAssets }} - Enable Local Assets + {{ else }}