diff --git a/CHANGELOG.md b/CHANGELOG.md index a153f455..98ae9896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ - Added Incident Reporting - Added Cypress tests - Added Github and Google OAuth login (beta) +- Added Delete All Failures +- Added Checkin form +- Added Pushover notifier # 0.90.22 - Added range input types for integer form fields diff --git a/frontend/src/API.js b/frontend/src/API.js index b88b2733..3bd07b97 100644 --- a/frontend/src/API.js +++ b/frontend/src/API.js @@ -68,6 +68,10 @@ class Api { return axios.post('api/reorder/services', data).then(response => (response.data)) } + async checkins() { + return axios.get('api/checkins').then(response => (response.data)) + } + async groups() { return axios.get('api/groups').then(response => (response.data)) } @@ -129,6 +133,14 @@ class Api { return axios.delete('api/incidents/'+incident.id).then(response => (response.data)) } + async checkin_create(data) { + return axios.post('api/checkins', data).then(response => (response.data)) + } + + async checkin_delete(checkin) { + return axios.delete('api/checkins/'+checkin.api_key).then(response => (response.data)) + } + async messages() { return axios.get('api/messages').then(response => (response.data)) } diff --git a/frontend/src/assets/scss/base.scss b/frontend/src/assets/scss/base.scss index a091a348..4670623d 100644 --- a/frontend/src/assets/scss/base.scss +++ b/frontend/src/assets/scss/base.scss @@ -15,13 +15,27 @@ HTML,BODY { } .copy-btn { + position: absolute; + right: 0; +} + +.btn-xs { + font-size: 8pt; + padding: 2px 6px; +} + +.copy-btn BUTTON { background-color: white; margin: 6px; height: 26px; font-size: 10pt; padding: 3px 7px; border: 1px solid #a7a7a7; - border-radius: 4px; + border-radius: 4px !important; +} + +.dim { + background-color: #f3f3f3; } .slider-info { diff --git a/frontend/src/components/Dashboard/TopNav.vue b/frontend/src/components/Dashboard/TopNav.vue index 26ab9e3c..7d00e4d2 100644 --- a/frontend/src/components/Dashboard/TopNav.vue +++ b/frontend/src/components/Dashboard/TopNav.vue @@ -26,10 +26,6 @@ - - Logout diff --git a/frontend/src/components/Service/ServiceInfo.vue b/frontend/src/components/Service/ServiceInfo.vue index 092e9763..679e644e 100644 --- a/frontend/src/components/Service/ServiceInfo.vue +++ b/frontend/src/components/Service/ServiceInfo.vue @@ -59,6 +59,10 @@ +
+ +
+
@@ -79,6 +83,7 @@ diff --git a/frontend/src/forms/Incident.vue b/frontend/src/forms/Incident.vue index 160e355f..9e1f51d7 100644 --- a/frontend/src/forms/Incident.vue +++ b/frontend/src/forms/Incident.vue @@ -8,11 +8,9 @@
-
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
+
-
+
-
+
+ + +
-
- - +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ {{error}} +
+ +
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
- {{error}} -
- - -
- +
diff --git a/frontend/src/pages/Settings.vue b/frontend/src/pages/Settings.vue index be06c3d5..215fddd8 100644 --- a/frontend/src/pages/Settings.vue +++ b/frontend/src/pages/Settings.vue @@ -73,7 +73,7 @@
-
+
@@ -86,7 +86,7 @@
-
+
diff --git a/frontend/src/store.js b/frontend/src/store.js index c06a4f56..17c79957 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -26,6 +26,7 @@ export default new Vuex.Store({ messages: [], users: [], notifiers: [], + checkins: [], admin: false }, getters: { @@ -39,6 +40,7 @@ export default new Vuex.Store({ incidents: state => state.incidents, users: state => state.users, notifiers: state => state.notifiers, + checkins: state => state.checkins, isAdmin: state => state.admin, @@ -48,6 +50,9 @@ export default new Vuex.Store({ groupsClean: state => state.groups.filter(g => g.name !== '').sort((a, b) => a.order_id - b.order_id), groupsCleanInOrder: state => state.groups.filter(g => g.name !== '').sort((a, b) => a.order_id - b.order_id).sort((a, b) => a.order_id - b.order_id), + serviceCheckins: (state) => (id) => { + return state.checkins.filter(c => c.service_id === id) + }, serviceByAll: (state) => (element) => { if (element % 1 === 0) { return state.services.find(s => s.id == element) @@ -99,6 +104,9 @@ export default new Vuex.Store({ setServices (state, services) { state.services = services }, + setCheckins (state, checkins) { + state.checkins = checkins + }, setGroups (state, groups) { state.groups = groups }, @@ -147,6 +155,8 @@ export default new Vuex.Store({ context.commit("setGroups", groups); const services = await Api.services() context.commit("setServices", services); + const checkins = await Api.checkins() + context.commit("setCheckins", checkins); const messages = await Api.messages() context.commit("setMessages", messages) context.commit("setHasPublicData", true) diff --git a/handlers/checkin.go b/handlers/checkin.go index db61edb7..c0d6eec3 100644 --- a/handlers/checkin.go +++ b/handlers/checkin.go @@ -40,8 +40,7 @@ func checkinCreateHandler(w http.ResponseWriter, r *http.Request) { return } checkin.ServiceId = service.Id - err = checkin.Create() - if err != nil { + if err := checkin.Create(); err != nil { sendErrorJson(err, w, r) return } @@ -52,7 +51,7 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) checkin, err := checkins.FindByAPI(vars["api"]) if err != nil { - sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r) + sendErrorJson(fmt.Errorf("checkin %s was not found", vars["api"]), w, r) return } ip, _, _ := net.SplitHostPort(r.RemoteAddr) @@ -60,15 +59,17 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) { hit := &checkins.CheckinHit{ Checkin: checkin.Id, From: ip, - CreatedAt: utils.Now().UTC(), + CreatedAt: utils.Now(), } + log.Infof("Checking %s was requested", checkin.Name) + err = hit.Create() if err != nil { sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r) return } checkin.Failing = false - checkin.LastHitTime = utils.Now().UTC() + checkin.LastHitTime = utils.Now() sendJsonAction(hit.Id, "update", w, r) } diff --git a/handlers/incident.go b/handlers/incident.go index dfa34171..2592dfa2 100644 --- a/handlers/incident.go +++ b/handlers/incident.go @@ -8,11 +8,6 @@ import ( "net/http" ) -func apiAllIncidentsHandler(w http.ResponseWriter, r *http.Request) { - inc := incidents.All() - returnJson(inc, w, r) -} - func apiServiceIncidentsHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) incids := incidents.FindByService(utils.ToInt(vars["id"])) diff --git a/handlers/routes.go b/handlers/routes.go index ff0ad564..fa8b357d 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -146,9 +146,9 @@ func Router() *mux.Router { // API CHECKIN Routes api.Handle("/api/checkins", authenticated(apiAllCheckinsHandler, false)).Methods("GET") - api.Handle("/api/checkin/{api}", authenticated(apiCheckinHandler, false)).Methods("GET") - api.Handle("/api/checkin", authenticated(checkinCreateHandler, false)).Methods("POST") - api.Handle("/api/checkin/{api}", authenticated(checkinDeleteHandler, false)).Methods("DELETE") + api.Handle("/api/checkins", authenticated(checkinCreateHandler, false)).Methods("POST") + api.Handle("/api/checkins/{api}", authenticated(apiCheckinHandler, false)).Methods("GET") + api.Handle("/api/checkins/{api}", authenticated(checkinDeleteHandler, false)).Methods("DELETE") r.Handle("/checkin/{api}", http.HandlerFunc(checkinHitHandler)) // Static Files Routes diff --git a/notifiers/notifiers.go b/notifiers/notifiers.go index ab9bb746..3f69ffcf 100644 --- a/notifiers/notifiers.go +++ b/notifiers/notifiers.go @@ -20,6 +20,7 @@ func InitNotifiers() { Twilio, Webhook, Mobile, + Pushover, ) } diff --git a/notifiers/pushover.go b/notifiers/pushover.go new file mode 100644 index 00000000..9ed7078d --- /dev/null +++ b/notifiers/pushover.go @@ -0,0 +1,85 @@ +package notifiers + +import ( + "fmt" + "github.com/statping/statping/types/failures" + "github.com/statping/statping/types/notifications" + "github.com/statping/statping/types/notifier" + "github.com/statping/statping/types/services" + "github.com/statping/statping/utils" + "net/url" + "strings" + "time" +) + +const ( + pushoverUrl = "https://api.pushover.net/1/messages.json" +) + +var _ notifier.Notifier = (*pushover)(nil) + +type pushover struct { + *notifications.Notification +} + +func (t *pushover) Select() *notifications.Notification { + return t.Notification +} + +var Pushover = &pushover{¬ifications.Notification{ + Method: "pushover", + Title: "Pushover", + Description: "Use Pushover to receive push notifications. You will need to create a New Application on Pushover before using this notifier.", + Author: "Hunter Long", + AuthorUrl: "https://github.com/hunterlong", + Icon: "fa dot-circle", + Delay: time.Duration(10 * time.Second), + Limits: 60, + Form: []notifications.NotificationForm{{ + Type: "text", + Title: "User Token", + Placeholder: "Insert your device's Pushover Token", + DbField: "api_key", + Required: true, + }, { + Type: "text", + Title: "Application API Key", + Placeholder: "Create an Application and insert the API Key here", + DbField: "api_secret", + Required: true, + }, + }}, +} + +// Send will send a HTTP Post to the Pushover API. It accepts type: string +func (t *pushover) sendMessage(message string) error { + v := url.Values{} + v.Set("token", t.ApiSecret) + v.Set("user", t.ApiKey) + v.Set("message", message) + rb := strings.NewReader(v.Encode()) + + _, _, err := utils.HttpRequest(pushoverUrl, "POST", "application/x-www-form-urlencoded", nil, rb, time.Duration(10*time.Second), true) + if err != nil { + return err + } + return err +} + +// OnFailure will trigger failing service +func (t *pushover) OnFailure(s *services.Service, f *failures.Failure) error { + msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name) + return t.sendMessage(msg) +} + +// OnSuccess will trigger successful service +func (t *pushover) OnSuccess(s *services.Service) error { + msg := fmt.Sprintf("Your service '%v' is currently online!", s.Name) + return t.sendMessage(msg) +} + +// OnTest will test the Pushover SMS messaging +func (t *pushover) OnTest() error { + msg := fmt.Sprintf("Testing the Pushover Notifier") + return t.sendMessage(msg) +} diff --git a/notifiers/pushover_test.go b/notifiers/pushover_test.go new file mode 100644 index 00000000..494bc19f --- /dev/null +++ b/notifiers/pushover_test.go @@ -0,0 +1,60 @@ +package notifiers + +import ( + "github.com/statping/statping/database" + "github.com/statping/statping/types/notifications" + "github.com/statping/statping/types/null" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +var ( + PUSHOVER_TOKEN = os.Getenv("PUSHOVER_TOKEN") + PUSHOVER_API = os.Getenv("PUSHOVER_API") +) + +func TestPushoverNotifier(t *testing.T) { + db, err := database.OpenTester() + require.Nil(t, err) + db.AutoMigrate(¬ifications.Notification{}) + notifications.SetDB(db) + + if PUSHOVER_TOKEN == "" || PUSHOVER_API == "" { + t.Log("Pushover notifier testing skipped, missing PUSHOVER_TOKEN and PUSHOVER_API environment variable") + t.SkipNow() + } + + t.Run("Load Pushover", func(t *testing.T) { + Pushover.ApiKey = PUSHOVER_TOKEN + Pushover.ApiSecret = PUSHOVER_API + Pushover.Enabled = null.NewNullBool(true) + + Add(Pushover) + + assert.Nil(t, err) + assert.Equal(t, "Hunter Long", Pushover.Author) + assert.Equal(t, PUSHOVER_TOKEN, Pushover.ApiKey) + }) + + t.Run("Pushover Within Limits", func(t *testing.T) { + assert.True(t, Pushover.CanSend()) + }) + + t.Run("Pushover OnFailure", func(t *testing.T) { + err := Pushover.OnFailure(exampleService, exampleFailure) + assert.Nil(t, err) + }) + + t.Run("Pushover OnSuccess", func(t *testing.T) { + err := Pushover.OnSuccess(exampleService) + assert.Nil(t, err) + }) + + t.Run("Pushover Test", func(t *testing.T) { + err := Pushover.OnTest() + assert.Nil(t, err) + }) + +} diff --git a/types/checkins/database.go b/types/checkins/database.go index 6732be18..2de053df 100644 --- a/types/checkins/database.go +++ b/types/checkins/database.go @@ -32,7 +32,7 @@ func All() []*Checkin { } func (c *Checkin) Create() error { - c.ApiKey = utils.RandomString(7) + c.ApiKey = utils.RandomString(32) q := db.Create(c) c.Start() diff --git a/types/configs/connection.go b/types/configs/connection.go index ea671e4e..29c5eaf4 100644 --- a/types/configs/connection.go +++ b/types/configs/connection.go @@ -57,7 +57,7 @@ func Connect(configs *DbConfig, retry bool) error { if err != nil { log.Debugln(fmt.Sprintf("Database connection error %s", err)) if retry { - log.Errorln(fmt.Sprintf("Database %s connection to '%s' is not available, trying again in 5 seconds...", configs.DbConn, configs.DbHost)) + log.Warnln(fmt.Sprintf("Database %s connection to '%s' is not available, trying again in 5 seconds...", configs.DbConn, configs.DbHost)) time.Sleep(5 * time.Second) return Connect(configs, retry) } else {