form use api routes - api updates

pull/99/head v0.79.7
Hunter Long 2018-11-13 11:28:21 -08:00
parent 23de9e16cc
commit 2eb5444a4e
23 changed files with 350 additions and 414 deletions

View File

@ -1,4 +1,4 @@
VERSION=0.79.6
VERSION=0.79.7
BINARY_NAME=statup
GOPATH:=$(GOPATH)
GOCMD=go

View File

@ -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(), "<title>Statup | Users</title>"))
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(), "<title>Statup | testadmin</title>"))
assert.True(t, strings.Contains(rr.Body.String(), "footer"))
assert.True(t, handlers.IsAuthenticated(req))

View File

@ -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

View File

@ -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 {

View File

@ -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, "<td>admin</td>")
assert.Contains(t, body, "<td>changedusername</td>")
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, "<title>Statup | Services</title>")
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, "</footer>")
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, "<title>Crystal Castles - Kept Status</title>")
assert.Contains(t, body, "<title>Google DNS Status</title>")
//assert.Contains(t, body, "</footer>")
}
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, "<title>Local Postgres Status</title>")
assert.Contains(t, body, "<title>Google DNS Status</title>")
//assert.Contains(t, body, "</footer>")
}
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, "<title>The Bravery - An Honest Mistake Status</title>")
//assert.Contains(t, body, "</footer>")
}
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()

View File

@ -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)

View File

@ -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))

View File

@ -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)
}

View File

@ -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)
})

View File

@ -405,6 +405,9 @@ HTML, BODY {
.pointer {
cursor: pointer; }
.jumbotron {
background-color: white; }
@media (max-width: 767px) {
HTML, BODY {
background-color: #fcfcfc; }

View File

@ -59,11 +59,23 @@ $('.test_notifier').on('click', function(e) {
});
$('form').submit(function() {
var spinner = '<i class="fa fa-spinner fa-spin"></i>';
$(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('<i class="fa fa-spinner fa-spin"></i>');
}
}
$('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 = `<tr id="service_${data.id}">
<td><span class="drag_icon d-none d-md-inline"><i class="fas fa-bars"></i></span> ${form.name}</td>
<td class="d-none d-md-table-cell">${data.online}<span class="badge badge-success">ONLINE</span></td>
<td class="text-right">
<div class="btn-group">
<a href="/service/${data.id}" class="btn btn-outline-secondary"><i class="fas fa-chart-area"></i> View</a>
<a href="/api/services/${data.id}" class="ajax_delete btn btn-danger confirm-btn" data-method="DELETE" data-obj="service_${data.id}" data-id="${data.id}"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>`;
$('#services_table').append(objTbl);
}
function CreateUser(output) {
console.log('creating user', output)
let form = output.form;
let data = output.data;
let objTbl = `<tr id="user_${data.id}">
<td>${form.username}</td>
<td class="text-right">
<div class="btn-group">
<a href="/user/${data.id}" class="btn btn-outline-secondary"><i class="fas fa-user-edit"></i> Edit</a>
<a href="/api/users/${data.id}" class="ajax_delete btn btn-danger confirm-btn" data-method="DELETE" data-obj="user_${data.id}" data-id="${data.id}"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>`;
$('#users_table').append(objTbl);
}
$('select#service_check_type').on('change', function() {
var selected = $('#service_check_type option:selected').val();
if (selected === 'POST') {

View File

@ -468,4 +468,8 @@ HTML,BODY {
cursor: pointer;
}
.jumbotron {
background-color: white;
}
@import 'mobile';

View File

@ -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
})

View File

@ -19,7 +19,14 @@
</div>
<div class="row mt-4">
<div class="col-12">
<h3>Services</h3>
{{if eq (len CoreApp.Services) 0}}
<div class="jumbotron jumbotron-fluid">
<div class="text-center">
<h1 class="display-4">No Services!</h1>
<a class="lead">You don't have any websites or applications being monitored by your Statup server. <p><a href="/services" class="btn btn-secondary mt-3">Add Service</a></p></p>
</div>
</div>
{{else}}
<div class="list-group mb-5 mt-3">
{{ range Services }}
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
@ -29,7 +36,8 @@
</div>
<p class="mb-1">{{.SmallText}}</p>
</a>
{{ end }}
{{ end }}
{{end}}
</div>
{{ range Services }}

View File

@ -1,6 +1,10 @@
{{define "form_message"}}
{{$message := .}}
<form action="{{if ne .Id 0}}/message/{{.Id}}{{else}}/messages{{end}}" method="POST">
{{if ne .Id 0}}
<form class="ajax_form" action="/api/messages/{{.Id}}" data-redirect="/messages" method="POST">
{{else}}
<form class="ajax_form" action="/api/messages" data-redirect="/messages" method="POST">
{{end}}
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">

View File

@ -1,5 +1,9 @@
{{define "form_service"}}
<form action="{{if ne .Id 0}}/service/{{.Id}}{{else}}/services{{end}}" method="POST">
{{if ne .Id 0}}
<form class="ajax_form" action="/api/services/{{.Id}}" data-redirect="/services" method="POST">
{{else}}
<form class="ajax_form" action="/api/services" data-redirect="/services" method="POST">
{{end}}
<div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8">

View File

@ -1,5 +1,9 @@
{{define "form_user"}}
<form action="{{if ne .Id 0}}/user/{{.Id}}{{else}}/users{{end}}" method="POST">
{{if ne .Id 0}}
<form class="ajax_form" action="/api/users/{{.Id}}" data-redirect="/users" method="POST">
{{else}}
<form class="ajax_form" action="/api/users" data-redirect="/users" method="POST">
{{end}}
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Username</label>
<div class="col-6 col-md-4">
@ -21,13 +25,13 @@
<div class="form-group row">
<label for="password" class="col-sm-4 col-form-label">Password</label>
<div class="col-sm-8">
<input type="password" name="password" class="form-control" id="password" value="##########" placeholder="Password" required>
<input type="password" name="password" class="form-control" id="password" {{if ne .Id 0}}value="##########"{{end}} placeholder="Password" required>
</div>
</div>
<div class="form-group row">
<label for="password_confirm" class="col-sm-4 col-form-label">Confirm Password</label>
<div class="col-sm-8">
<input type="password" name="password_confirm" class="form-control" id="password_confirm" value="##########" placeholder="Confirm Password" required>
<input type="password" name="password_confirm" class="form-control" id="password_confirm" {{if ne .Id 0}}value="##########"{{end}} placeholder="Confirm Password" required>
</div>
</div>
<div class="form-group row">
@ -35,5 +39,6 @@
<button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update User{{else}}Create User{{end}}</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
{{end}}
{{end}}

View File

@ -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 <a href="https://github.com/hunterlong/statup_plugin">https://github.com/hunterlong/statup_plugin</a>.

View File

@ -16,14 +16,14 @@
</thead>
<tbody>
{{range .}}
<tr>
<tr id="message_{{.Id}}">
<td>{{.Title}}</td>
<td>{{if .Service}}<a href="/service/{{.Service.Id}}">{{.Service.Name}}</a>{{end}}</td>
<td>{{Duration 0}}</td>
<td class="text-right" id="message_{{.Id}}">
<td class="text-right">
<div class="btn-group">
<a href="/message/{{.Id}}" class="btn btn-outline-secondary"><i class="fas fa-exclamation-triangle"></i> Edit</a>
<a href="/message/{{.Id}}/delete" class="btn btn-danger confirm-btn" data-id="message_{{.Id}}"><i class="fas fa-times"></i></a>
<a href="/api/messages/{{.Id}}" class="ajax_delete btn btn-danger confirm-btn" data-method="DELETE" data-obj="message_{{.Id}}" data-id="{{.Id}}"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>

View File

@ -5,8 +5,8 @@
<div class="col-12">
{{if ne (len .) 0}}
<h3>Services</h3>
<table class="table">
<thead>
<tr>
@ -15,21 +15,22 @@
<th scope="col"></th>
</tr>
</thead>
<tbody class="sortable">
<tbody class="sortable" id="services_table">
{{range .}}
<tr id="{{.Id}}">
<tr id="service_{{.Id}}">
<td><span class="drag_icon d-none d-md-inline"><i class="fas fa-bars"></i></span> {{.Name}}</td>
<td class="d-none d-md-table-cell">{{if .Online}}<span class="badge badge-success">ONLINE</span>{{else}}<span class="badge badge-danger">OFFLINE</span>{{end}} </td>
<td class="text-right">
<div class="btn-group">
<a href="/service/{{.Id}}" class="btn btn-outline-secondary"><i class="fas fa-chart-area"></i> View</a>
<a href="/service/{{.Id}}/delete" class="btn btn-danger confirm-btn"><i class="fas fa-times"></i></a>
<a href="/api/services/{{.Id}}" class="ajax_delete btn btn-danger confirm-btn" data-method="DELETE" data-obj="service_{{.Id}}" data-id="{{.Id}}"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
<h3>Create Service</h3>

View File

@ -127,7 +127,12 @@
<div class="tab-pane" id="v-pills-style" role="tabpanel" aria-labelledby="v-pills-style-tab">
{{if not .UsingAssets }}
<a href="/settings/build" class="btn btn-primary btn-block"{{if USE_CDN}} disabled{{end}}>Enable Local Assets</a>
<div class="jumbotron jumbotron-fluid">
<div class="text-center col-12">
<h1 class="display-5">Enable Local Assets</h1>
<a class="lead">Customize your status page design by enabling local assets. This will create a 'assets' directory containing all CSS.<p><a href="/settings/build" class="btn btn-primary mt-3"{{if USE_CDN}} disabled{{end}}>Enable Local Assets</a></p></p>
</div>
</div>
{{ else }}
<form method="POST" action="/settings/css">
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">

View File

@ -11,14 +11,14 @@
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tbody id="users_table">
{{range .}}
<tr>
<tr id="user_{{.Id}}">
<td>{{.Username}}</td>
<td class="text-right" id="user_{{.Id}}">
<td class="text-right">
<div class="btn-group">
<a href="/user/{{.Id}}" class="btn btn-outline-secondary"><i class="fas fa-user-edit"></i> Edit</a>
<a href="/user/{{.Id}}/delete" class="btn btn-danger confirm-btn" data-id="user_{{.Id}}"><i class="fas fa-times"></i></a>
<a href="/api/users/{{.Id}}" class="ajax_delete btn btn-danger confirm-btn" data-method="DELETE" data-obj="user_{{.Id}}" data-id="{{.Id}}"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>

View File

@ -22,12 +22,12 @@ import (
// User is the main struct for Users
type User struct {
Id int64 `gorm:"primary_key;column:id" json:"id"`
Username string `gorm:"type:varchar(100);unique;column:username;" json:"username"`
Password string `gorm:"column:password" json:"-"`
Email string `gorm:"type:varchar(100);unique;column:email" json:"-"`
ApiKey string `gorm:"column:api_key" json:"api_key"`
Username string `gorm:"type:varchar(100);unique;column:username;" json:"username,omitempty"`
Password string `gorm:"column:password" json:"password,omitempty"`
Email string `gorm:"type:varchar(100);unique;column:email" json:"email,omitempty"`
ApiKey string `gorm:"column:api_key" json:"api_key,omitempty"`
ApiSecret string `gorm:"column:api_secret" json:"-"`
Admin NullBool `gorm:"column:administrator" json:"admin"`
Admin NullBool `gorm:"column:administrator" json:"admin,omitempty"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
UserInterface `gorm:"-" json:"-"`