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 BINARY_NAME=statup
GOPATH:=$(GOPATH) GOPATH:=$(GOPATH)
GOCMD=go GOCMD=go

View File

@ -110,7 +110,6 @@ func TestRunAll(t *testing.T) {
}) })
t.Run(dbt+" Select Core", func(t *testing.T) { t.Run(dbt+" Select Core", func(t *testing.T) {
RunSelectCoreMYQL(t, dbt) RunSelectCoreMYQL(t, dbt)
t.Log(core.CoreApp)
}) })
t.Run(dbt+" Select Services", func(t *testing.T) { t.Run(dbt+" Select Services", func(t *testing.T) {
RunSelectAllMysqlServices(t) RunSelectAllMysqlServices(t)
@ -556,6 +555,7 @@ func RunUsersHandler(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
route.ServeHTTP(rr, req) 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(), "<title>Statup | Users</title>"))
assert.True(t, strings.Contains(rr.Body.String(), "footer")) assert.True(t, strings.Contains(rr.Body.String(), "footer"))
assert.True(t, handlers.IsAuthenticated(req)) assert.True(t, handlers.IsAuthenticated(req))
@ -566,6 +566,7 @@ func RunUserViewHandler(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
route.ServeHTTP(rr, req) 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(), "<title>Statup | testadmin</title>"))
assert.True(t, strings.Contains(rr.Body.String(), "footer")) assert.True(t, strings.Contains(rr.Body.String(), "footer"))
assert.True(t, handlers.IsAuthenticated(req)) assert.True(t, handlers.IsAuthenticated(req))

View File

@ -32,6 +32,13 @@ func ReturnUser(u *types.User) *user {
return &user{u} 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. // SelectUser returns the user based on the user's ID.
func SelectUser(id int64) (*user, error) { func SelectUser(id int64) (*user, error) {
var user user var user user

View File

@ -17,6 +17,7 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
@ -30,9 +31,10 @@ import (
type apiResponse struct { type apiResponse struct {
Status string `json:"status"` Status string `json:"status"`
Object string `json:"type"` Object string `json:"type,omitempty"`
Id int64 `json:"id"` Id int64 `json:"id,omitempty"`
Method string `json:"method"` Method string `json:"method,omitempty"`
Error string `json:"error,omitempty"`
} }
func apiIndexHandler(w http.ResponseWriter, r *http.Request) { 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.ApiSecret = utils.NewSHA1Hash(40)
core.CoreApp, err = core.UpdateCore(core.CoreApp) core.CoreApp, err = core.UpdateCore(core.CoreApp)
if err != nil { if err != nil {
utils.Log(3, err) sendErrorJson(err, w, r)
return
} }
http.Redirect(w, r, "/settings", http.StatusSeeOther) http.Redirect(w, r, "/settings", http.StatusSeeOther)
} }
@ -84,7 +87,7 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"])) service := core.SelectService(utils.StringInt(vars["id"]))
if service == nil { if service == nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) sendErrorJson(errors.New("service data not found"), w, r)
return return
} }
fields := parseGet(r) fields := parseGet(r)
@ -106,7 +109,7 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"])) service := core.SelectService(utils.StringInt(vars["id"]))
if service == nil { if service == nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) sendErrorJson(errors.New("service not found"), w, r)
return return
} }
fields := parseGet(r) fields := parseGet(r)
@ -127,7 +130,7 @@ func apiServiceHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectServicer(utils.StringInt(vars["id"])) service := core.SelectServicer(utils.StringInt(vars["id"]))
if service == nil { if service == nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) sendErrorJson(errors.New("service not found"), w, r)
return return
} }
@ -144,13 +147,13 @@ func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&service) err := decoder.Decode(&service)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
newService := core.ReturnService(service) newService := core.ReturnService(service)
_, err = newService.Create(true) _, err = newService.Create(true)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -165,7 +168,7 @@ func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"])) service := core.SelectService(utils.StringInt(vars["id"]))
if service == nil { if service == nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) sendErrorJson(errors.New("service not found"), w, r)
return return
} }
var updatedService *types.Service var updatedService *types.Service
@ -175,7 +178,7 @@ func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) {
service = core.ReturnService(updatedService) service = core.ReturnService(updatedService)
err := service.Update(true) err := service.Update(true)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
service.Check(true) service.Check(true)
@ -191,12 +194,12 @@ func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"])) service := core.SelectService(utils.StringInt(vars["id"]))
if service == nil { if service == nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) sendErrorJson(errors.New("service not found"), w, r)
return return
} }
err := service.Delete() err := service.Delete()
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
output := apiResponse{ output := apiResponse{
@ -223,113 +226,6 @@ func apiAllServicesHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(servicesOut) 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) { func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) { if !isAPIAuthorized(r) {
sendUnauthorizedJson(w, r) sendUnauthorizedJson(w, r)
@ -338,7 +234,7 @@ func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
_, notifierObj, err := notifier.SelectNotifier(vars["notifier"]) _, notifierObj, err := notifier.SelectNotifier(vars["notifier"])
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("%v notifier was not found", vars["notifier"]), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
w.Header().Set("Content-Type", "application/json") 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"]) notifer, not, err := notifier.SelectNotifier(vars["notifier"])
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("%v notifier was not found", vars["notifier"]), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
@ -374,7 +270,8 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
_, err = notifier.Update(not, notifer) _, err = notifier.Update(not, notifer)
if err != nil { if err != nil {
utils.Log(3, fmt.Sprintf("issue updating notifier: %v", err)) sendErrorJson(err, w, r)
return
} }
notifier.OnSave(notifer.Method) notifier.OnSave(notifer.Method)
@ -389,7 +286,7 @@ func apiAllMessagesHandler(w http.ResponseWriter, r *http.Request) {
} }
messages, err := core.SelectMessages() messages, err := core.SelectMessages()
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("error fetching all messages: %v", err), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -404,7 +301,7 @@ func apiMessageGetHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
message, err := core.SelectMessage(utils.StringInt(vars["id"])) message, err := core.SelectMessage(utils.StringInt(vars["id"]))
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("message #%v was not found", vars["id"]), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -419,12 +316,12 @@ func apiMessageDeleteHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
message, err := core.SelectMessage(utils.StringInt(vars["id"])) message, err := core.SelectMessage(utils.StringInt(vars["id"]))
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("message #%v was not found", vars["id"]), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
err = message.Delete() err = message.Delete()
if err != nil { 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 return
} }
@ -447,14 +344,14 @@ func apiMessageUpdateHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
message, err := core.SelectMessage(utils.StringInt(vars["id"])) message, err := core.SelectMessage(utils.StringInt(vars["id"]))
if err != nil { 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 return
} }
var messageBody *types.Message var messageBody *types.Message
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&messageBody) err = decoder.Decode(&messageBody)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
@ -462,7 +359,7 @@ func apiMessageUpdateHandler(w http.ResponseWriter, r *http.Request) {
message = core.ReturnMessage(messageBody) message = core.ReturnMessage(messageBody)
_, err = message.Update() _, err = message.Update()
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) sendErrorJson(err, w, r)
return return
} }
@ -512,14 +409,23 @@ func apiServiceFailuresHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(service.AllFailures()) 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) { func sendUnauthorizedJson(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{ output := apiResponse{
"error": "unauthorized", Status: "error",
"url": r.RequestURI, Error: errors.New("not authorized").Error(),
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(data) json.NewEncoder(w).Encode(output)
} }
func isAPIAuthorized(r *http.Request) bool { func isAPIAuthorized(r *http.Request) bool {

View File

@ -16,7 +16,6 @@
package handlers package handlers
import ( import (
"fmt"
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/core/notifier"
_ "github.com/hunterlong/statup/notifiers" _ "github.com/hunterlong/statup/notifiers"
@ -175,53 +174,6 @@ func TestServicesHandler(t *testing.T) {
assert.True(t, isRouteAuthenticated(req)) 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) { func TestUsersHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/users", nil) req, err := http.NewRequest("GET", "/users", nil)
assert.Nil(t, err) assert.Nil(t, err)
@ -275,48 +227,6 @@ func TestHelpHandler(t *testing.T) {
assert.True(t, isRouteAuthenticated(req)) 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) { func TestServicesHandler2(t *testing.T) {
req, err := http.NewRequest("GET", "/services", nil) req, err := http.NewRequest("GET", "/services", nil)
assert.Nil(t, err) assert.Nil(t, err)
@ -325,36 +235,36 @@ func TestServicesHandler2(t *testing.T) {
body := rr.Body.String() body := rr.Body.String()
assert.Equal(t, 200, rr.Code) assert.Equal(t, 200, rr.Code)
assert.Contains(t, body, "<title>Statup | Services</title>") assert.Contains(t, body, "<title>Statup | Services</title>")
assert.Contains(t, body, "Crystal Castles - Kept") assert.Contains(t, body, "JSON Users Test")
assert.Contains(t, body, "Local Postgres") assert.Contains(t, body, "JSON API Tester")
//assert.Contains(t, body, "</footer>") //assert.Contains(t, body, "</footer>")
assert.True(t, isRouteAuthenticated(req)) assert.True(t, isRouteAuthenticated(req))
} }
func TestViewHTTPServicesHandler(t *testing.T) { 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) assert.Nil(t, err)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
Router().ServeHTTP(rr, req) Router().ServeHTTP(rr, req)
body := rr.Body.String() body := rr.Body.String()
assert.Equal(t, 200, rr.Code) 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>") //assert.Contains(t, body, "</footer>")
} }
func TestViewTCPServicesHandler(t *testing.T) { 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) assert.Nil(t, err)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
Router().ServeHTTP(rr, req) Router().ServeHTTP(rr, req)
body := rr.Body.String() body := rr.Body.String()
assert.Equal(t, 200, rr.Code) 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>") //assert.Contains(t, body, "</footer>")
} }
func TestServicesDeleteFailuresHandler(t *testing.T) { 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) assert.Nil(t, err)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
Router().ServeHTTP(rr, req) Router().ServeHTTP(rr, req)
@ -363,43 +273,7 @@ func TestServicesDeleteFailuresHandler(t *testing.T) {
} }
func TestFailingServicesDeleteFailuresHandler(t *testing.T) { func TestFailingServicesDeleteFailuresHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/service/1/delete_failures", nil) req, err := http.NewRequest("GET", "/service/5/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)
assert.Nil(t, err) assert.Nil(t, err)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
Router().ServeHTTP(rr, req) Router().ServeHTTP(rr, req)
@ -486,7 +360,7 @@ func TestPrometheusHandler(t *testing.T) {
Router().ServeHTTP(rr, req) Router().ServeHTTP(rr, req)
body := rr.Body.String() body := rr.Body.String()
assert.Equal(t, 200, rr.Code) 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)) assert.True(t, isRouteAuthenticated(req))
} }
@ -622,42 +496,6 @@ func TestExportHandler(t *testing.T) {
assert.True(t, isRouteAuthenticated(req)) 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 { func isRouteAuthenticated(req *http.Request) bool {
os.Setenv("GO_ENV", "production") os.Setenv("GO_ENV", "production")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()

View File

@ -107,8 +107,8 @@ func TestApiAllServicesHandlerHandler(t *testing.T) {
var obj []types.Service var obj []types.Service
formatJSON(body, &obj) formatJSON(body, &obj)
assert.Equal(t, 200, rr.Code) assert.Equal(t, 200, rr.Code)
assert.Equal(t, "Test Service 9", obj[0].Name) assert.Equal(t, "Google", obj[0].Name)
assert.Equal(t, "https://www.youtube.com/watch?v=yydZbVoCbn0&t=870s", obj[0].Domain) assert.Equal(t, "https://google.com", obj[0].Domain)
} }
func TestApiServiceHandler(t *testing.T) { func TestApiServiceHandler(t *testing.T) {
@ -212,7 +212,6 @@ func TestApiViewUserHandler(t *testing.T) {
func TestApiUpdateUserHandler(t *testing.T) { func TestApiUpdateUserHandler(t *testing.T) {
data := `{ data := `{
"username": "adminupdated", "username": "adminupdated",
"email": "info@email.com",
"password": "password123", "password": "password123",
"admin": true}` "admin": true}`
rr, err := httpRequestAPI(t, "POST", "/api/users/1", strings.NewReader(data)) rr, err := httpRequestAPI(t, "POST", "/api/users/1", strings.NewReader(data))
@ -220,6 +219,7 @@ func TestApiUpdateUserHandler(t *testing.T) {
body := rr.Body.String() body := rr.Body.String()
var obj types.User var obj types.User
formatJSON(body, &obj) formatJSON(body, &obj)
t.Log(body)
assert.Equal(t, 200, rr.Code) assert.Equal(t, 200, rr.Code)
assert.Equal(t, "adminupdated", obj.Username) assert.Equal(t, "adminupdated", obj.Username)
assert.Equal(t, true, obj.Admin.Bool) assert.Equal(t, true, obj.Admin.Bool)

View File

@ -65,17 +65,10 @@ func Router() *mux.Router {
// USER Routes // USER Routes
r.Handle("/users", http.HandlerFunc(usersHandler)).Methods("GET") 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(usersEditHandler)).Methods("GET")
r.Handle("/user/{id}", http.HandlerFunc(updateUserHandler)).Methods("POST")
r.Handle("/user/{id}/delete", http.HandlerFunc(usersDeleteHandler)).Methods("GET")
// MESSAGES Routes // MESSAGES Routes
r.Handle("/messages", http.HandlerFunc(messagesHandler)).Methods("GET") 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 // SETTINGS Routes
r.Handle("/settings", http.HandlerFunc(settingsHandler)).Methods("GET") r.Handle("/settings", http.HandlerFunc(settingsHandler)).Methods("GET")
@ -89,14 +82,13 @@ func Router() *mux.Router {
// SERVICE Routes // SERVICE Routes
r.Handle("/services", http.HandlerFunc(servicesHandler)).Methods("GET") 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("/services/reorder", http.HandlerFunc(reorderServiceHandler)).Methods("POST")
r.Handle("/service/{id}", http.HandlerFunc(servicesViewHandler)).Methods("GET") 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}/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}/delete_failures", http.HandlerFunc(servicesDeleteFailuresHandler)).Methods("GET")
r.Handle("/service/{id}/checkin", http.HandlerFunc(checkinCreateHandler)).Methods("POST") 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}/delete", http.HandlerFunc(checkinDeleteHandler)).Methods("GET")
r.Handle("/checkin/{id}", http.HandlerFunc(checkinHitHandler)) r.Handle("/checkin/{id}", http.HandlerFunc(checkinHitHandler))

View File

@ -16,7 +16,8 @@
package handlers package handlers
import ( import (
"fmt" "encoding/json"
"errors"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/types" "github.com/hunterlong/statup/types"
@ -45,82 +46,116 @@ func usersEditHandler(w http.ResponseWriter, r *http.Request) {
executeResponse(w, r, "user.html", user, nil) executeResponse(w, r, "user.html", user, nil)
} }
func updateUserHandler(w http.ResponseWriter, r *http.Request) { func apiUserHandler(w http.ResponseWriter, r *http.Request) {
if !IsAuthenticated(r) { if !isAPIAuthorized(r) {
http.Redirect(w, r, "/", http.StatusSeeOther) sendUnauthorizedJson(w, r)
return return
} }
r.ParseForm()
vars := mux.Vars(r) vars := mux.Vars(r)
id := utils.StringInt(vars["id"]) user, err := core.SelectUser(utils.StringInt(vars["id"]))
user, err := core.SelectUser(id)
if err != nil { if err != nil {
utils.Log(3, fmt.Sprintf("user error: %v", err)) sendErrorJson(err, w, r)
w.WriteHeader(http.StatusInternalServerError)
return return
} }
user.Password = ""
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
user.Username = r.PostForm.Get("username") func apiUserUpdateHandler(w http.ResponseWriter, r *http.Request) {
user.Email = r.PostForm.Get("email") if !isAPIAuthorized(r) {
isAdmin := r.PostForm.Get("admin") == "on" sendUnauthorizedJson(w, r)
user.Admin = types.NewNullBool(isAdmin) return
password := r.PostForm.Get("password")
if password != "##########" {
user.Password = utils.HashPassword(password)
} }
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() err = user.Update()
if err != nil { if err != nil {
utils.Log(3, err) sendErrorJson(err, w, r)
}
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)
return return
} }
r.ParseForm() w.Header().Set("Content-Type", "application/json")
username := r.PostForm.Get("username") json.NewEncoder(w).Encode(user)
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")
} }
func usersDeleteHandler(w http.ResponseWriter, r *http.Request) { func apiUserDeleteHandler(w http.ResponseWriter, r *http.Request) {
if !IsAuthenticated(r) { if !isAPIAuthorized(r) {
http.Redirect(w, r, "/", http.StatusSeeOther) sendUnauthorizedJson(w, r)
return return
} }
vars := mux.Vars(r) vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"]) users := core.CountUsers()
user, _ := core.SelectUser(int64(id)) if users == 1 {
sendErrorJson(errors.New("cannot delete the last user"), w, r)
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)
return return
} }
user.Delete() user, err := core.SelectUser(utils.StringInt(vars["id"]))
http.Redirect(w, r, "/users", http.StatusSeeOther) 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 ( 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"}` webhookMessage = `{"id": "%service.Id","name": "%service.Name","online": "%service.Online","issue": "%failure.Issue"}`
apiKey = "application/json" apiKey = "application/json"
fullMsg string fullMsg string
@ -99,7 +99,7 @@ func TestWebhookNotifier(t *testing.T) {
t.Run("webhooker Send", func(t *testing.T) { t.Run("webhooker Send", func(t *testing.T) {
err := webhook.Send(fullMsg) err := webhook.Send(fullMsg)
assert.Nil(t, err) assert.Error(t, err)
assert.Equal(t, len(webhook.Queue), 1) assert.Equal(t, len(webhook.Queue), 1)
}) })

View File

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

View File

@ -59,11 +59,23 @@ $('.test_notifier').on('click', function(e) {
}); });
$('form').submit(function() { $('form').submit(function() {
var spinner = '<i class="fa fa-spinner fa-spin"></i>'; // Spinner($(this).find('button[type=submit]'))
$(this).find('button[type=submit]').prop('disabled', true);
$(this).find('button[type=submit]').html(spinner);
}); });
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() { $('select#service_type').on('change', function() {
var selected = $('#service_type option:selected').val(); var selected = $('#service_type option:selected').val();
var typeLabel = $('#service_type_label'); 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() { $('select#service_check_type').on('change', function() {
var selected = $('#service_check_type option:selected').val(); var selected = $('#service_check_type option:selected').val();
if (selected === 'POST') { if (selected === 'POST') {

View File

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

View File

@ -199,7 +199,7 @@ func CopyAllToPublic(box *rice.Box, folder string) error {
if err != nil { if err != nil {
return nil return nil
} }
filePath := fmt.Sprintf("%v%v", folder, path) filePath := fmt.Sprintf("%v/%v", folder, path)
SaveAsset(file, utils.Directory, filePath) SaveAsset(file, utils.Directory, filePath)
return nil return nil
}) })

View File

@ -19,7 +19,14 @@
</div> </div>
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12"> <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"> <div class="list-group mb-5 mt-3">
{{ range Services }} {{ range Services }}
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start"> <a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
@ -29,7 +36,8 @@
</div> </div>
<p class="mb-1">{{.SmallText}}</p> <p class="mb-1">{{.SmallText}}</p>
</a> </a>
{{ end }} {{ end }}
{{end}}
</div> </div>
{{ range Services }} {{ range Services }}

View File

@ -1,6 +1,10 @@
{{define "form_message"}} {{define "form_message"}}
{{$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"> <div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Title</label> <label for="username" class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8"> <div class="col-sm-8">

View File

@ -1,5 +1,9 @@
{{define "form_service"}} {{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"> <div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Service Name</label> <label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8"> <div class="col-sm-8">

View File

@ -1,5 +1,9 @@
{{define "form_user"}} {{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"> <div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Username</label> <label for="username" class="col-sm-4 col-form-label">Username</label>
<div class="col-6 col-md-4"> <div class="col-6 col-md-4">
@ -21,13 +25,13 @@
<div class="form-group row"> <div class="form-group row">
<label for="password" class="col-sm-4 col-form-label">Password</label> <label for="password" class="col-sm-4 col-form-label">Password</label>
<div class="col-sm-8"> <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> </div>
<div class="form-group row"> <div class="form-group row">
<label for="password_confirm" class="col-sm-4 col-form-label">Confirm Password</label> <label for="password_confirm" class="col-sm-4 col-form-label">Confirm Password</label>
<div class="col-sm-8"> <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> </div>
<div class="form-group row"> <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> <button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update User{{else}}Create User{{end}}</button>
</div> </div>
</div> </div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form> </form>
{{end}} {{end}}

View File

@ -17,9 +17,6 @@ You can change multiple settings in your Statup instance.
# Users # Users
Users can access the Statup Dashboard to add, remove, and view services. Users can access the Statup Dashboard to add, remove, and view services.
# Notifications
# Plugins # 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. 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>. 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> </thead>
<tbody> <tbody>
{{range .}} {{range .}}
<tr> <tr id="message_{{.Id}}">
<td>{{.Title}}</td> <td>{{.Title}}</td>
<td>{{if .Service}}<a href="/service/{{.Service.Id}}">{{.Service.Name}}</a>{{end}}</td> <td>{{if .Service}}<a href="/service/{{.Service.Id}}">{{.Service.Name}}</a>{{end}}</td>
<td>{{Duration 0}}</td> <td>{{Duration 0}}</td>
<td class="text-right" id="message_{{.Id}}"> <td class="text-right">
<div class="btn-group"> <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}}" 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> </div>
</td> </td>
</tr> </tr>

View File

@ -5,8 +5,8 @@
<div class="col-12"> <div class="col-12">
{{if ne (len .) 0}}
<h3>Services</h3> <h3>Services</h3>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -15,21 +15,22 @@
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody class="sortable"> <tbody class="sortable" id="services_table">
{{range .}} {{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><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="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"> <td class="text-right">
<div class="btn-group"> <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}}" 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> </div>
</td> </td>
</tr> </tr>
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
{{end}}
<h3>Create Service</h3> <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"> <div class="tab-pane" id="v-pills-style" role="tabpanel" aria-labelledby="v-pills-style-tab">
{{if not .UsingAssets }} {{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 }} {{ else }}
<form method="POST" action="/settings/css"> <form method="POST" action="/settings/css">
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist"> <ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">

View File

@ -11,14 +11,14 @@
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="users_table">
{{range .}} {{range .}}
<tr> <tr id="user_{{.Id}}">
<td>{{.Username}}</td> <td>{{.Username}}</td>
<td class="text-right" id="user_{{.Id}}"> <td class="text-right">
<div class="btn-group"> <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}}" 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> </div>
</td> </td>
</tr> </tr>

View File

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