Merge branch 'master' into feature/tls-renegotiation

pull/482/head
Anže Jenšterle 2020-04-12 15:04:54 +02:00 committed by GitHub
commit 274b82c3b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 1163 additions and 626 deletions

19
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,19 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- bug
- urgent
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions. If this is still an problem, please create a new issue.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: true

View File

@ -10,13 +10,13 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@master uses: actions/checkout@master
- name: Create a Sentry.io release - name: Create a Sentry release
uses: tclindner/sentry-releases-action@v1.0.0 uses: tclindner/sentry-releases-action@v1.0.0
env: env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }} SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: Statping SENTRY_ORG: statping
SENTRY_PROJECT: golang SENTRY_PROJECT: backend
with: with:
tagName: ${{ github.ref }} tagName: ${{ github.ref }}
environment: qa environment: qa

22
.github/workflows/sentry_frontend.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: ReleaseFrontendWorkflow
on:
release:
types: [published, prereleased]
jobs:
createSentryRelease:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Create a Sentry Frontend release
uses: tclindner/sentry-releases-action@v1.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_FRONTEND_AUTH_TOKEN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: statping
SENTRY_PROJECT: frontend
with:
tagName: ${{ github.ref }}
environment: qa

View File

@ -10,12 +10,6 @@ before_script:
branches: branches:
only: only:
- master - master
cache:
directories:
- ~/.npm
- ~/.cache
- $GOPATH/pkg/mod
- $GOPATH/src/github.com/statping/statping/frontend/node_modules
env: env:
global: global:
- "PATH=$HOME/.local/bin:$PATH" - "PATH=$HOME/.local/bin:$PATH"
@ -23,7 +17,6 @@ env:
- DB_USER=travis - DB_USER=travis
- DB_PASS= - DB_PASS=
- DB_DATABASE=test - DB_DATABASE=test
- GO_ENV=test
- STATPING_DIR=$GOPATH/src/github.com/statping/statping - STATPING_DIR=$GOPATH/src/github.com/statping/statping
go: 1.14 go: 1.14
go_import_path: github.com/statping/statping go_import_path: github.com/statping/statping
@ -36,6 +29,7 @@ install:
- "make test-deps yarn clean compile install" - "make test-deps yarn clean compile install"
language: go language: go
addons: addons:
chrome: stable
apt: apt:
packages: packages:
- libgconf-2-4 - libgconf-2-4

View File

@ -1,7 +1,19 @@
# 0.90.25
- Added string response on OnTest for Notifiers
- Modified UI to show user the response for a Notifier.
- Modified some Notifiers title's
- Added more Cypress e2e testing
# 0.90.24
- Fixed login form from not showing
# 0.90.23 # 0.90.23
- Added Incident Reporting - Added Incident Reporting
- Added Cypress tests - Added Cypress tests
- Added Github and Google OAuth login (beta) - Added Github and Google OAuth login (beta)
- Added Delete All Failures
- Added Checkin form
- Added Pushover notifier
# 0.90.22 # 0.90.22
- Added range input types for integer form fields - Added range input types for integer form fields

View File

@ -34,7 +34,7 @@ test: clean
release: test-deps release: test-deps
wget -O statping.gpg $(SIGN_URL) wget -O statping.gpg $(SIGN_URL)
gpg --import statping.gpg gpg --import statping.gpg
make build-all make build-all upload_to_s3
test-ci: clean compile test-deps test-ci: clean compile test-deps
SASS=`which sass` go test -v -covermode=count -coverprofile=coverage.out -p=1 ./... SASS=`which sass` go test -v -covermode=count -coverprofile=coverage.out -p=1 ./...
@ -260,11 +260,10 @@ publish-dev:
publish-homebrew: publish-homebrew:
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(PUBLISH_BODY) https://api.travis-ci.com/repo/statping%2Fhomebrew-statping/requests curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token $(TRAVIS_API)" -d $(PUBLISH_BODY) https://api.travis-ci.com/repo/statping%2Fhomebrew-statping/requests
upload_to_s3: upload_to_s3: travis_s3_creds
aws s3 cp ./source/css $(ASSETS_BKT) --recursive --exclude "*" --include "*.css" aws s3 cp ./source/dist/css $(ASSETS_BKT) --recursive --exclude "*" --include "*.css"
aws s3 cp ./source/js $(ASSETS_BKT) --recursive --exclude "*" --include "*.js" aws s3 cp ./source/dist/js $(ASSETS_BKT) --recursive --exclude "*" --include "*.js"
aws s3 cp ./source/font $(ASSETS_BKT) --recursive --exclude "*" --include "*.eot" --include "*.svg" --include "*.woff" --include "*.woff2" --include "*.ttf" --include "*.css" aws s3 cp ./source/dist/scss $(ASSETS_BKT) --recursive --exclude "*" --include "*.scss"
aws s3 cp ./source/scss $(ASSETS_BKT) --recursive --exclude "*" --include "*.scss"
aws s3 cp ./install.sh $(ASSETS_BKT) aws s3 cp ./install.sh $(ASSETS_BKT)
travis_s3_creds: travis_s3_creds:

View File

@ -132,6 +132,7 @@ func TestAssetsCLI(t *testing.T) {
} }
func TestSassCLI(t *testing.T) { func TestSassCLI(t *testing.T) {
t.SkipNow()
catchCLI([]string{"sass"}) catchCLI([]string{"sass"})
assert.FileExists(t, dir+"/assets/css/main.css") assert.FileExists(t, dir+"/assets/css/main.css")
assert.FileExists(t, dir+"/assets/css/style.css") assert.FileExists(t, dir+"/assets/css/style.css")

View File

@ -28,8 +28,7 @@ var (
verboseMode int verboseMode int
port int port int
log = utils.Log.WithField("type", "cmd") log = utils.Log.WithField("type", "cmd")
confgs *configs.DbConfig
confgs *configs.DbConfig
) )
// parseFlags will parse the application flags // parseFlags will parse the application flags
@ -75,8 +74,6 @@ func main() {
parseFlags() parseFlags()
utils.SentryInit(VERSION)
if err := source.Assets(); err != nil { if err := source.Assets(); err != nil {
exit(err) exit(err)
} }
@ -202,18 +199,14 @@ func InitApp() error {
if _, err := core.Select(); err != nil { if _, err := core.Select(); err != nil {
return err return err
} }
if _, err := services.SelectAllServices(true); err != nil { if _, err := services.SelectAllServices(true); err != nil {
return err return err
} }
go services.CheckServices() go services.CheckServices()
notifiers.InitNotifiers() notifiers.InitNotifiers()
go database.Maintenance()
utils.SentryInit(&VERSION, core.App.AllowReports.Bool)
core.App.Setup = true core.App.Setup = true
core.App.Started = utils.Now() core.App.Started = utils.Now()
go database.Maintenance()
return nil return nil
} }

View File

@ -8,10 +8,9 @@
"DB_PASS": "password123", "DB_PASS": "password123",
"GO_ENV": "production" "GO_ENV": "production"
}, },
"baseUrl": "http://localhost:8888",
"chromeWebSecurity": false, "chromeWebSecurity": false,
"defaultCommandTimeout": 15000, "defaultCommandTimeout": 30000,
"requestTimeout": 15000, "requestTimeout": 30000,
"watchForFileChanges": false, "watchForFileChanges": false,
"failOnStatusCode": false "failOnStatusCode": false
} }

View File

@ -2,6 +2,12 @@
context('Setup Process', () => { context('Setup Process', () => {
it('should be not be setup yet', () => {
cy.request(`/api`).then((response) => {
expect(response.body).to.have.property('setup', false)
})
})
it('should setup Statping with SQLite', () => { it('should setup Statping with SQLite', () => {
cy.visit('/setup', {failOnStatusCode: false}) cy.visit('/setup', {failOnStatusCode: false})
cy.get('#db_connection').select('sqlite') cy.get('#db_connection').select('sqlite')
@ -19,8 +25,34 @@ context('Setup Process', () => {
it('should have sample data', () => { it('should have sample data', () => {
cy.visit('/') cy.visit('/')
cy.get('#title').should('contain', 'Demo Tester')
cy.get('#description').should('contain', 'This is a test from Crypress!')
cy.get('.card').should('have.length', 5) cy.get('.card').should('have.length', 5)
cy.get('.group_header').should('have.length', 2) cy.get('.group_header').should('have.length', 2)
}) })
it('should be completely setup', () => {
cy.request(`/api`).then((response) => {
expect(response.body).to.have.property('setup', true)
expect(response.body).to.have.property('domain', 'http://localhost:8888')
})
})
it('should be able to Login', () => {
cy.visit('/login')
cy.get('#username').clear().type('admin')
cy.get('#password').clear().type('admin')
cy.get('button[type="submit"]').click()
cy.get('.navbar-brand').should('contain', 'Statping')
cy.getCookies()
cy.getCookies().should('have.length', 1)
cy.request(`/api`).then((response) => {
expect(response.body).to.have.property('admin', true)
expect(response.body).to.have.property('logged_in', true)
})
})
}) })

View File

@ -29,7 +29,7 @@ context('Groups Tests', () => {
cy.visit('/dashboard/services') cy.visit('/dashboard/services')
cy.get('.sortable_groups > tr').should('have.length', 3) cy.get('.sortable_groups > tr').should('have.length', 3)
cy.get('.sortable_groups > tr').eq(0).contains('PRIVATE') cy.get('.sortable_groups > tr').eq(0).contains('PUBLIC')
cy.get('.sortable_groups > tr').eq(1).contains('PUBLIC') cy.get('.sortable_groups > tr').eq(1).contains('PUBLIC')
cy.get('.sortable_groups > tr').eq(2).contains('PRIVATE') cy.get('.sortable_groups > tr').eq(2).contains('PRIVATE')
}) })
@ -48,17 +48,28 @@ context('Groups Tests', () => {
cy.get('button[type="submit"]').click() cy.get('button[type="submit"]').click()
}) })
it('should edit Group', () => {
cy.visit('/dashboard/services')
cy.get('.sortable_groups > tr').eq(0).find('.btn-outline-secondary').click()
cy.get('#title').should('have.value', 'Test Group')
cy.get('#title').clear().type('Updated Group')
cy.get('button[type="submit"]').click()
cy.get('.sortable_groups > tr').eq(0).contains('Updated Group')
})
it('should confirm new groups', () => { it('should confirm new groups', () => {
cy.visit('/dashboard/services') cy.visit('/dashboard/services')
cy.get('.sortable_groups > tr').should('have.length', 5) cy.get('.sortable_groups > tr').should('have.length', 5)
cy.get('.sortable_groups > tr').eq(0).contains('PUBLIC') cy.get('.sortable_groups > tr').eq(0).contains('PUBLIC')
cy.get('.sortable_groups > tr').eq(0).contains('Test Group') cy.get('.sortable_groups > tr').eq(0).contains('Updated Group')
cy.get('.sortable_groups > tr').eq(1).contains('PRIVATE') cy.get('.sortable_groups > tr').eq(1).contains('PRIVATE')
cy.get('.sortable_groups > tr').eq(1).contains('Test Private Group') cy.get('.sortable_groups > tr').eq(1).contains('Test Private Group')
}) })
it('should delete new groups', () => { it('should delete new groups', () => {
cy.visit('/dashboard/services')
cy.get('.sortable_groups > tr').eq(0).find('.btn-danger').click() cy.get('.sortable_groups > tr').eq(0).find('.btn-danger').click()
cy.get('.sortable_groups > tr').eq(1).find('.btn-danger').click() cy.get('.sortable_groups > tr').eq(1).find('.btn-danger').click()
cy.get('.sortable_groups > tr').should('have.length', 3) cy.get('.sortable_groups > tr').should('have.length', 3)

View File

@ -2,7 +2,7 @@
import "../support/commands" import "../support/commands"
context('Messages Tests', () => { context('Annoucements Tests', () => {
beforeEach(() => { beforeEach(() => {
@ -43,4 +43,10 @@ context('Messages Tests', () => {
cy.get('tbody > tr').should('have.length', 3) cy.get('tbody > tr').should('have.length', 3)
}) })
it('should confirm delete Message', () => {
cy.visit('/dashboard/messages')
cy.get('tbody > tr').eq(0).find('.btn-danger').click()
cy.get('tbody > tr').should('have.length', 2)
})
}) })

View File

@ -25,12 +25,26 @@ context('Notifier Tests', () => {
cy.getCookies().should('have.length', 1) cy.getCookies().should('have.length', 1)
}) })
// uzrwstmtd69hi4wgzsj27q2v29mtpu
it('should confirm notifiers are installed', () => { it('should confirm notifiers are installed', () => {
cy.visit('/dashboard/settings') cy.visit('/dashboard/settings')
cy.get('#notifiers_tabs > a').should('have.length', 9) cy.get('#notifiers_tabs > a').should('have.length', 10)
cy.get('#api_key').should('not.have.value', '') cy.get('#api_key').should('not.have.value', '')
cy.get('#api_secret').should('not.have.value', '') cy.get('#api_secret').should('not.have.value', '')
}) })
// it('should test and save notifier', () => {
// cy.visit('/dashboard/settings')
// cy.get('#notifiers_tabs > a').should('have.length', 10)
// cy.get('#notifiers_tabs > #v-pills-command-tab').click()
//
// cy.get('#v-pills-command-tab > .form-control').eq(0).clear().type('/bin/echo')
// cy.get('#v-pills-command-tab > .form-control').eq(1).clear().type('"success"')
// cy.get('#v-pills-command-tab > .form-control').eq(2).clear().type('"failure"')
//
// cy.get('#v-pills-command-tab').find(".save-notifier").click()
// cy.get('#v-pills-command-tab').find(".test-notifier").click()
// })
}) })

View File

@ -26,7 +26,7 @@ context('Services Tests', () => {
it('should goto services', () => { it('should goto services', () => {
cy.visit('/dashboard/services') cy.visit('/dashboard/services')
cy.get('#services_list > tr').should('have.length', 5) cy.get('#services_list > tr').should('have.length', 6)
cy.get('.sortable_groups > tr').should('have.length', 3) cy.get('.sortable_groups > tr').should('have.length', 3)
}) })
@ -79,7 +79,7 @@ context('Services Tests', () => {
it('should create new ICMP service', () => { it('should create new ICMP service', () => {
cy.visit('/dashboard/create_service') cy.visit('/dashboard/create_service')
cy.get('#name').clear().type('ICMP Service') cy.get('#name').clear().type('ICMP Service')
cy.get('#service_type').select('icmp') cy.get('#service_type').select('icmp')
cy.get('#service_url').clear().type('8.8.8.8') cy.get('#service_url').clear().type('8.8.8.8')
@ -93,16 +93,20 @@ context('Services Tests', () => {
it('should confirm new services', () => { it('should confirm new services', () => {
cy.visit('/dashboard/services') cy.visit('/dashboard/services')
cy.get('#services_list > tr').should('have.length', 9) cy.get('#services_list > tr').should('have.length', 10)
}) })
it('should delete new services', () => { it('should delete new services', () => {
cy.visit('/dashboard/services') cy.visit('/dashboard/services')
cy.get('#services_list > tr').eq(0).find('.btn-danger').click() cy.get('#services_list > tr').should('have.length', 10)
cy.get('#services_list > tr').eq(0).find('.btn-danger').click() cy.get('#services_list > tr').eq(0).find('a.btn-danger').click()
cy.get('#services_list > tr').eq(0).find('.btn-danger').click() cy.get('#services_list > tr').should('have.length', 9)
cy.get('#services_list > tr').eq(0).find('.btn-danger').click() cy.get('#services_list > tr').eq(1).find('a.btn-danger').click()
cy.get('#services_list > tr').should('have.length', 4) cy.get('#services_list > tr').should('have.length', 8)
cy.get('#services_list > tr').eq(2).find('a.btn-danger').click()
cy.get('#services_list > tr').should('have.length', 7)
cy.get('#services_list > tr').eq(3).find('a.btn-danger').click()
cy.get('#services_list > tr').should('have.length', 6)
}) })
}) })

View File

@ -27,7 +27,7 @@ context('Settings Tests', () => {
it('should confirm notifiers are installed', () => { it('should confirm notifiers are installed', () => {
cy.visit('/dashboard/settings') cy.visit('/dashboard/settings')
cy.get('#notifiers_tabs > a').should('have.length', 9) cy.get('#notifiers_tabs > a').should('have.length', 10)
cy.get('#api_key').should('not.have.value', '') cy.get('#api_key').should('not.have.value', '')
cy.get('#api_secret').should('not.have.value', '') cy.get('#api_secret').should('not.have.value', '')
@ -36,7 +36,7 @@ context('Settings Tests', () => {
it('should update Statping settings', () => { it('should update Statping settings', () => {
cy.visit('/dashboard/settings') cy.visit('/dashboard/settings')
cy.get('#project').clear().type('Statping Cypress Tests') cy.get('#project').clear().type('Statping Updated')
cy.get('#description').clear().type('Statping can use Cypress e2e testing to make it more stable!') cy.get('#description').clear().type('Statping can use Cypress e2e testing to make it more stable!')
cy.get('#domain').clear().type('http://localhost:8888') cy.get('#domain').clear().type('http://localhost:8888')
cy.get('#footer').clear().type('Statping Custom Footer') cy.get('#footer').clear().type('Statping Custom Footer')
@ -46,7 +46,7 @@ context('Settings Tests', () => {
it('should confirm Statping settings', () => { it('should confirm Statping settings', () => {
cy.visit('/dashboard/settings') cy.visit('/dashboard/settings')
cy.get('#project').should('have.value', 'Statping Cypress Tests') cy.get('#project').should('have.value', 'Statping Updated')
cy.get('#description').should('have.value', 'Statping can use Cypress e2e testing to make it more stable!') cy.get('#description').should('have.value', 'Statping can use Cypress e2e testing to make it more stable!')
cy.get('#domain').should('have.value', 'http://localhost:8888') cy.get('#domain').should('have.value', 'http://localhost:8888')
cy.get('#footer').should('have.value', 'Statping Custom Footer') cy.get('#footer').should('have.value', 'Statping Custom Footer')
@ -59,6 +59,13 @@ context('Settings Tests', () => {
cy.get('.footer').should('contain', 'Statping Custom Footer') cy.get('.footer').should('contain', 'Statping Custom Footer')
}) })
it('should regenerate API Keys', () => {
cy.visit('/dashboard/settings')
cy.get('#regenkeys').click()
cy.get('#api_key').should('not.have.value', '')
cy.get('#api_secret').should('not.have.value', '')
})
it('should create Local Assets', () => { it('should create Local Assets', () => {
cy.visit('/dashboard/settings') cy.visit('/dashboard/settings')
cy.get('#v-pills-style-tab').click() cy.get('#v-pills-style-tab').click()

View File

@ -39,13 +39,39 @@ context('Users Tests', () => {
cy.get('#password_confirm').clear().type('password123') cy.get('#password_confirm').clear().type('password123')
cy.get('button[type="submit"]').click() cy.get('button[type="submit"]').click()
cy.get('#users_table > tr').should('have.length', 2)
}) })
// it('should confirm new user', () => { it('should create new Admin User', () => {
// cy.visit('/dashboard/users') cy.visit('/dashboard/users')
// cy.get('#users_table > tr').should('have.length', 2) cy.get('#username').clear().type('admin3')
// cy.get('#users_table > tr').eq(0).contains('admin') cy.get('#admin_switch').click()
// cy.get('#users_table > tr').eq(1).contains('admin2') cy.get('#email').clear().type('info@admin3.com')
// }) cy.get('#password').clear().type('password123')
cy.get('#password_confirm').clear().type('password123')
cy.get('button[type="submit"]').click()
cy.get('#users_table > tr').should('have.length', 3)
})
it('should confirm new user', () => {
cy.visit('/dashboard/users')
cy.get('#users_table > tr').should('have.length', 3)
cy.get('#users_table > tr').eq(0).contains('admin')
cy.get('#users_table > tr').eq(1).contains('admin2')
cy.get('#users_table > tr').eq(2).contains('admin3')
cy.get('#users_table > tr').eq(0).contains('ADMIN')
cy.get('#users_table > tr').eq(1).contains('USER')
cy.get('#users_table > tr').eq(2).contains('ADMIN')
})
it('should delete new users', () => {
cy.visit('/dashboard/users')
cy.get('#users_table > tr').should('have.length', 3)
cy.get('#users_table > tr').eq(2).find('a.btn-danger').click()
cy.get('#users_table > tr').eq(1).find('a.btn-danger').click()
cy.get('#users_table > tr').should('have.length', 1)
})
}) })

View File

@ -18,4 +18,12 @@
module.exports = (on, config) => { module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits // `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config // `config` is the resolved Cypress config
on("before:browser:launch", (browser = {}, args) => {
if (browser.name === "chrome") {
// ^ make sure this is your browser name, you may
// be using 'canary' or 'chromium' for example, so change it to match!
args.push("--proxy-bypass-list=<-loopback>");
return args;
}
});
} }

View File

@ -9,9 +9,9 @@
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint",
"backend-test": "newman run -e ../dev/postman_environment.json --delay-request 500 ../dev/postman.json", "backend-test": "newman run -e ../dev/postman_environment.json --delay-request 500 ../dev/postman.json",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:test": "cypress run --record --key 49d99e5e-04c6-46df-beef-54b68e152a4d", "cypress:test": "cypress run --browser chrome --record false --key $CYPRESS_KEY",
"test": "start-server-and-test start http://0.0.0.0:8888/api cypress:test", "test": "start-server-and-test start http://localhost:8080/api cypress:test",
"start": "statping -port 8888 > /dev/null 2>&1" "start": "statping -port 8080"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free-solid": "^5.1.0-3", "@fortawesome/fontawesome-free-solid": "^5.1.0-3",

View File

@ -1,7 +1,11 @@
import Vue from "vue";
import axios from 'axios' import axios from 'axios'
import * as Sentry from "@sentry/browser";
import * as Integrations from "@sentry/integrations";
const qs = require('querystring');
const qs = require('querystring')
const tokenKey = "statping_user"; const tokenKey = "statping_user";
const errorReporter = "https://bed4d75404924cb3a799e370733a1b64@sentry.statping.com/3"
class Api { class Api {
constructor() { constructor() {
@ -9,7 +13,11 @@ class Api {
} }
async core() { async core() {
return axios.get('api').then(response => (response.data)) const core = axios.get('api').then(response => (response.data))
if (core.allow_reports) {
await this.sentry_init()
}
return core
} }
async core_save(obj) { async core_save(obj) {
@ -68,6 +76,10 @@ class Api {
return axios.post('api/reorder/services', data).then(response => (response.data)) return axios.post('api/reorder/services', data).then(response => (response.data))
} }
async checkins() {
return axios.get('api/checkins').then(response => (response.data))
}
async groups() { async groups() {
return axios.get('api/groups').then(response => (response.data)) return axios.get('api/groups').then(response => (response.data))
} }
@ -114,7 +126,7 @@ class Api {
} }
async incident_update_delete(update) { async incident_update_delete(update) {
return axios.post('api/incidents/'+incident.id+'/updates', data).then(response => (response.data)) return axios.delete('api/incidents/'+update.incident+'/updates/'+update.id).then(response => (response.data))
} }
async incidents_service(service) { async incidents_service(service) {
@ -129,6 +141,14 @@ class Api {
return axios.delete('api/incidents/'+incident.id).then(response => (response.data)) 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() { async messages() {
return axios.get('api/messages').then(response => (response.data)) return axios.get('api/messages').then(response => (response.data))
} }
@ -246,6 +266,13 @@ class Api {
await axios.all([all]) await axios.all([all])
} }
async sentry_init() {
Sentry.init({
dsn: errorReporter,
integrations: [new Integrations.Vue({Vue, attachProps: true})],
});
}
} }
const api = new Api() const api = new Api()
export default api export default api

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="app"> <div id="app">
<router-view :app="app" :loaded="loaded"/> <router-view :loaded="loaded"/>
<Footer :logged_in="logged_in" :version="version" v-if="$route.path !== '/setup'"/> <Footer v-if="$route.path !== '/setup'"/>
</div> </div>
</template> </template>
@ -18,35 +18,38 @@
return { return {
loaded: false, loaded: false,
version: "", version: "",
logged_in: false,
app: null
} }
}, },
async created() { computed: {
this.app = await this.$store.dispatch('loadRequired') core() {
return this.$store.getters.core
}
},
async beforeMount() {
await this.$store.dispatch('loadCore')
this.app = {...this.$store.state} if (!this.core.setup) {
if (this.$store.getters.core.logged_in) {
await this.$store.dispatch('loadAdmin')
}
this.loaded = true
if (!this.$store.getters.core.setup) {
this.$router.push('/setup') this.$router.push('/setup')
} }
window.console.log('finished loadRequired') if (this.$route.path !== '/setup') {
if (this.core.logged_in) {
await this.$store.dispatch('loadAdmin')
} else {
await this.$store.dispatch('loadRequired')
}
this.loaded = true
}
}, },
async mounted() { async mounted() {
if (this.$route.path !== '/setup') { if (this.$route.path !== '/setup') {
const tk = localStorage.getItem("statping_user") const tk = localStorage.getItem("statping_user")
if (this.$store.getters.core.logged_in) { if (this.core.logged_in) {
this.logged_in = true this.logged_in = true
await this.$store.dispatch('loadAdmin') await this.$store.dispatch('loadAdmin')
} }
} }
},
methods: {
} }
} }
</script> </script>

View File

@ -15,13 +15,27 @@ HTML,BODY {
} }
.copy-btn { .copy-btn {
position: absolute;
right: 0;
}
.btn-xs {
font-size: 8pt;
padding: 2px 6px;
}
.copy-btn BUTTON {
background-color: white; background-color: white;
margin: 6px; margin: 6px;
height: 26px; height: 26px;
font-size: 10pt; font-size: 10pt;
padding: 3px 7px; padding: 3px 7px;
border: 1px solid #a7a7a7; border: 1px solid #a7a7a7;
border-radius: 4px; border-radius: 4px !important;
}
.dim {
background-color: #f3f3f3;
} }
.slider-info { .slider-info {
@ -333,6 +347,7 @@ HTML,BODY {
.card { .card {
background-color: $service-background; background-color: $service-background;
border: $service-border; border: $service-border;
//box-shadow: 0px 2px 11px 1px rgba(0, 0, 0, 0.13);
} }
.card-body { .card-body {

View File

@ -16,7 +16,7 @@
</div> </div>
</div> </div>
<div v-for="(service, index) in $store.getters.services" class="service_block" v-bind:key="index"> <div v-for="(service, index) in services" class="service_block" v-bind:key="index">
<ServiceInfo :service=service /> <ServiceInfo :service=service />
</div> </div>
</div> </div>
@ -30,10 +30,21 @@
components: { components: {
ServiceInfo ServiceInfo
}, },
data() {
return {
visible: false
}
},
computed: {
services() {
return this.$store.getters.services
}
},
methods: { methods: {
failuresLast24Hours() { failuresLast24Hours() {
let total = 0; let total = 0;
this.$store.getters.services.map((s) => { this.services.map((s) => {
total += s.failures_24_hours total += s.failures_24_hours
}) })
return total return total

View File

@ -24,7 +24,7 @@
</thead> </thead>
<draggable tag="tbody" v-model="groupsList" class="sortable_groups" handle=".drag_icon"> <draggable tag="tbody" v-model="groupsList" class="sortable_groups" handle=".drag_icon">
<tr v-for="(group, index) in $store.getters.groupsCleanInOrder" v-bind:key="group.id"> <tr v-for="(group, index) in groupsList" v-bind:key="group.id">
<td><span class="drag_icon d-none d-md-inline"> <td><span class="drag_icon d-none d-md-inline">
<font-awesome-icon icon="bars" class="mr-3" /></span> {{group.name}} <font-awesome-icon icon="bars" class="mr-3" /></span> {{group.name}}
</td> </td>
@ -49,7 +49,6 @@
</div> </div>
</div> </div>
<FormGroup v-if="$store.state.admin" :edit="editChange" :in_group="group"/> <FormGroup v-if="$store.state.admin" :edit="editChange" :in_group="group"/>
</div> </div>
@ -92,9 +91,6 @@
this.$store.commit('setGroups', groups) this.$store.commit('setGroups', groups)
} }
} }
},
beforeMount() {
}, },
methods: { methods: {
editChange(v) { editChange(v) {
@ -105,13 +101,6 @@
this.group = g this.group = g
this.edit = !mode this.edit = !mode
}, },
reordered_services() {
},
saveUpdatedOrder: function (e) {
window.console.log("saving...");
window.console.log(this.myViews.array()); // this.myViews.array is not a function
},
async deleteGroup(g) { async deleteGroup(g) {
let c = confirm(`Are you sure you want to delete '${g.name}'?`) let c = confirm(`Are you sure you want to delete '${g.name}'?`)
if (c) { if (c) {

View File

@ -14,15 +14,22 @@
</thead> </thead>
<tbody id="users_table"> <tbody id="users_table">
<tr v-for="(user, index) in $store.getters.users" v-bind:key="index" > <tr v-for="(user, index) in users" v-bind:key="user.id" >
<td>{{user.username}}</td> <td>{{user.username}}</td>
<td v-if="user.admin"><span class="badge badge-danger">ADMIN</span></td> <td>
<td v-if="!user.admin"><span class="badge badge-primary">USER</span></td> <span class="badge" :class="{'badge-danger': user.admin, 'badge-primary': !user.admin}">
{{user.admin ? 'ADMIN' : 'USER'}}
</span>
</td>
<td class="d-none d-md-table-cell">{{niceDate(user.updated_at)}}</td> <td class="d-none d-md-table-cell">{{niceDate(user.updated_at)}}</td>
<td class="text-right"> <td class="text-right">
<div class="btn-group"> <div class="btn-group">
<a @click.prevent="editUser(user, edit)" href="" class="btn btn-outline-secondary"><font-awesome-icon icon="user" /> Edit</a> <a @click.prevent="editUser(user, edit)" href="#" class="btn btn-outline-secondary edit-user">
<a @click.prevent="deleteUser(user)" v-if="index !== 0" href="" class="btn btn-danger"><font-awesome-icon icon="times" /></a> <font-awesome-icon icon="user" /> Edit
</a>
<a @click.prevent="deleteUser(user)" v-if="index !== 0" href="#" class="btn btn-danger delete-user">
<font-awesome-icon icon="times" />
</a>
</div> </div>
</td> </td>
</tr> </tr>
@ -49,6 +56,11 @@
user: {} user: {}
} }
}, },
computed: {
users() {
return this.$store.getters.users
}
},
methods: { methods: {
editChange(v) { editChange(v) {
this.user = {} this.user = {}

View File

@ -64,28 +64,20 @@ export default {
} }
} }
}, },
data() {
return {
}
},
methods: { methods: {
async updateOrder(value) { async updateOrder(value) {
let data = []; let data = [];
value.forEach((s, k) => { value.forEach((s, k) => {
data.push({ service: s.id, order: k + 1 }) data.push({ service: s.id, order: k + 1 })
}); });
const reorder = await Api.services_reorder(data) await Api.services_reorder(data)
window.console.log('reorder', reorder) await this.update()
const services = await Api.services()
this.$store.commit('setServices', services)
}, },
async deleteService(s) { async deleteService(s) {
let c = confirm(`Are you sure you want to delete '${s.name}'?`) let c = confirm(`Are you sure you want to delete '${s.name}'?`)
if (c) { if (c) {
await Api.service_delete(s.id) await Api.service_delete(s.id)
const services = await Api.services() await this.update()
this.$store.commit('setServices', services)
} }
}, },
serviceGroup(s) { serviceGroup(s) {
@ -95,6 +87,10 @@ export default {
} }
return "" return ""
}, },
async update() {
const services = await Api.services()
this.$store.commit('setServices', services)
}
} }
} }
</script> </script>

View File

@ -59,10 +59,9 @@
components: { components: {
codemirror codemirror
}, },
props: { computed: {
core: { core() {
type: Object, return this.$store.getters.core
required: true
} }
}, },
data () { data () {
@ -86,11 +85,6 @@
} }
} }
}, },
computed: {
codemirror () {
}
},
async mounted () { async mounted () {
await this.fetchTheme() await this.fetchTheme()
this.changeTab('vars') this.changeTab('vars')

View File

@ -26,10 +26,6 @@
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item"> <li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/logs" class="nav-link">Logs</router-link> <router-link to="/dashboard/logs" class="nav-link">Logs</router-link>
</li> </li>
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/help" class="nav-link">Help</router-link>
</li>
</ul> </ul>
<span class="navbar-text"> <span class="navbar-text">
<a href="#" class="nav-link" v-on:click="logout">Logout</a> <a href="#" class="nav-link" v-on:click="logout">Logout</a>
@ -55,7 +51,7 @@
this.$store.commit('setHasAllData', false) this.$store.commit('setHasAllData', false)
this.$store.commit('setToken', null) this.$store.commit('setToken', null)
this.$store.commit('setAdmin', false) this.$store.commit('setAdmin', false)
await this.$router.push('/') await this.$router.push('/logout')
} }
} }
} }

View File

@ -0,0 +1,45 @@
<template>
<button v-html="loading ? loadLabel : label" @click.prevent="runAction" type="submit" :disabled="loading || disabled" class="btn btn-block" :class="{class: !loading, 'btn-outline-light': loading}">
</button>
</template>
<script>
export default {
name: 'LoadButton',
props: {
action: {
type: Function,
required: true
},
label: {
type: String,
required: true
},
class: {
type: String,
default: "btn-primary"
},
disabled: {
type: Boolean,
default: false
},
},
data() {
return {
loading: false,
loadLabel: "<div class=\"spinner-border text-dark\"><span class=\"sr-only\">Loading</span></div>"
}
},
methods: {
async runAction() {
this.loading = true;
await this.action();
this.loading = false;
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,12 +1,12 @@
<template> <template>
<footer> <footer>
<div v-if="!$store.getters.core.footer" class="footer text-center mb-4 p-2"> <div v-if="!core.footer" class="footer text-center mb-4 p-2">
<a href="https://github.com/statping/statping" target="_blank"> <a href="https://github.com/statping/statping" target="_blank">
Statping {{$store.getters.core.version}} made with <font-awesome-icon style="color: #d40d0d" icon="heart"/> Statping {{core.version}} made with <font-awesome-icon icon="heart"/>
</a> | </a> |
<router-link :to="$store.getters.core.logged_in ? '/dashboard' : '/login'">Dashboard</router-link> <router-link :to="core.logged_in ? '/dashboard' : '/login'">Dashboard</router-link>
</div> </div>
<div v-else class="footer text-center mb-4 p-2" v-html="$store.getters.core.footer"></div> <div v-else class="footer text-center mb-4 p-2" v-html="core.footer"></div>
</footer> </footer>
</template> </template>
@ -18,15 +18,11 @@
components: { components: {
Dashboard Dashboard
}, },
props: { computed: {
version: String, core() {
logged_in: Boolean return this.$store.getters.core
},
watch: {
logged_in() {
}
} }
}
} }
</script> </script>

View File

@ -26,7 +26,6 @@ export default {
components: { components: {
IncidentsBlock, IncidentsBlock,
GroupServiceFailures GroupServiceFailures
}, },
props: { props: {
group: Object group: Object

View File

@ -34,7 +34,7 @@ export default {
}, },
methods: { methods: {
async lastDaysFailures() { async lastDaysFailures() {
const start = this.nowSubtract(86400 * 30) const start = this.nowSubtract(86400 * 30)
this.failureData = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(this.now()), "24h") this.failureData = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(this.now()), "24h")
} }
} }

View File

@ -1,13 +1,18 @@
<template> <template>
<div> <div>
<h1 id="title" class="col-12 text-center pt-4 mt-4 mb-3 header-title font-6">{{$store.getters.core.name}}</h1> <h1 id="title" class="col-12 text-center pt-4 mt-4 mb-3 header-title font-6">{{core.name}}</h1>
<h5 id="description" class="col-12 text-center mb-5 header-desc font-3">{{$store.getters.core.description}}</h5> <h5 id="description" class="col-12 text-center mb-5 header-desc font-3">{{core.description}}</h5>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'Header', name: 'Header',
computed: {
core() {
return this.$store.getters.core
}
}
} }
</script> </script>

View File

@ -35,10 +35,12 @@
} }
}, },
async mounted() { async mounted() {
this.value = this.func.value; if (this.func) {
this.title = this.func.title; this.value = this.func.value;
this.subtitle = this.func.subtitle; this.title = this.func.title;
this.chart = this.convertToChartData(this.func.chart); this.subtitle = this.func.subtitle;
this.chart = this.convertToChartData(this.func.chart);
}
}, },
async latencyYesterday() { async latencyYesterday() {
const todayTime = await Api.service_hits(this.service.id, this.toUnix(this.nowSubtract(86400)), this.toUnix(new Date()), this.group, false) const todayTime = await Api.service_hits(this.service.id, this.toUnix(this.nowSubtract(86400)), this.toUnix(new Date()), this.group, false)

View File

@ -9,7 +9,7 @@
<p class="mb-1">{{failure.issue}}</p> <p class="mb-1">{{failure.issue}}</p>
</div> </div>
<nav v-if="total > 4" aria-label="page navigation example"> <nav v-if="total > 4" class="mt-3">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
<li class="page-item" :class="{'disabled': page===1}"> <li class="page-item" :class="{'disabled': page===1}">
<a @click.prevent="gotoPage(page-1)" :disabled="page===1" class="page-link" href="#" aria-label="Previous"> <a @click.prevent="gotoPage(page-1)" :disabled="page===1" class="page-link" href="#" aria-label="Previous">
@ -17,7 +17,7 @@
<span class="sr-only">Previous</span> <span class="sr-only">Previous</span>
</a> </a>
</li> </li>
<li v-for="n in Math.floor(total / limit)" class="page-item" :class="{'active': page === n}"> <li v-for="n in maxPages" class="page-item" :class="{'active': page === n}">
<a @click.prevent="gotoPage(n)" class="page-link" href="#">{{n}}</a> <a @click.prevent="gotoPage(n)" class="page-link" href="#">{{n}}</a>
</li> </li>
<li class="page-item" :class="{'disabled': page===Math.floor(total / limit)}"> <li class="page-item" :class="{'disabled': page===Math.floor(total / limit)}">
@ -53,6 +53,19 @@ export default {
page: 1 page: 1
} }
}, },
computed: {
pages() {
return Math.floor(this.total / this.limit)
},
maxPages() {
const p = Math.floor(this.total / this.limit)
if (p > 16) {
return 16
} else {
return p
}
}
},
async mounted () { async mounted () {
await this.gotoPage(1) await this.gotoPage(1)
}, },

View File

@ -1,23 +1,28 @@
<template v-if="service"> <template>
<div class="col-12 card mb-4" style="min-height: 280px;" :class="{'offline-card': !service.online}"> <div class="card mb-4" :class="{'offline-card': !service.online}">
<div class="card-body p-3 p-md-1 pt-md-3 pb-md-1"> <div class="card-title px-4 pt-3">
<h4 class="card-title mb-4"> <h4 v-observe-visibility="setVisible">
<router-link :to="serviceLink(service)">{{service.name}}</router-link> <router-link :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge float-right" :class="{'badge-success': service.online, 'badge-danger': !service.online}"> <span class="badge float-right" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
{{service.online ? "ONLINE" : "OFFLINE"}} {{service.online ? "ONLINE" : "OFFLINE"}}
</span> </span>
</h4> </h4>
</div>
<div class="card-body p-3 p-md-1 pt-md-3 pb-md-1">
<transition name="fade"> <transition name="fade">
<div v-if="loaded && service.online" class="row pb-3"> <div v-if="loaded && service.online" class="col-12 pb-2">
<div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-3"> <div class="row">
<ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/> <div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-3">
</div> <ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/>
<div class="col-md-6 col-sm-12 mt-4 mt-md-0 mb-3"> </div>
<ServiceSparkLine :title="set1_name" subtitle="Latency Last 7 Days" :series="set1"/> <div class="col-md-6 col-sm-12 mt-4 mt-md-0 mb-3">
<ServiceSparkLine :title="set1_name" subtitle="Latency Last 7 Days" :series="set1"/>
</div>
</div> </div>
<div class="d-none row col-12 mt-4 pt-1 mb-3 align-content-center"> <div v-if="false" class="row mt-4 pt-1 mb-3 align-content-center">
<StatsGen :service="service" <StatsGen :service="service"
title="Since Yesterday" title="Since Yesterday"
@ -44,21 +49,35 @@
group="24h" expression="latencyPercent"/> group="24h" expression="latencyPercent"/>
</div> </div>
<div class="col-4"> </div>
<button @click.prevent="Tab('incident')" class="btn btn-block btn-outline-secondary incident" :class="{'text-white btn-secondary': openTab==='incident'}" >Incidents</button> </transition>
</div> </div>
<div class="col-4">
<div class="card-footer">
<div class="row">
<div class="col-3">
<button @click.prevent="Tab('incident')" class="btn btn-block btn-outline-secondary incident" :class="{'text-white btn-secondary': openTab==='incident'}" >Incidents</button>
</div>
<div class="col-3">
<button @click.prevent="Tab('checkin')" class="btn btn-block btn-outline-secondary checkin" :class="{'text-white btn-secondary': openTab==='checkin'}" >Checkins</button> <button @click.prevent="Tab('checkin')" class="btn btn-block btn-outline-secondary checkin" :class="{'text-white btn-secondary': openTab==='checkin'}" >Checkins</button>
</div> </div>
<div class="col-4"> <div class="col-3">
<button @click.prevent="Tab('failures')" class="btn btn-block btn-outline-secondary failures" :disabled="service.stats.failures === 0" :class="{'text-white btn-secondary': openTab==='failures'}"> <button @click.prevent="Tab('failures')" class="btn btn-block btn-outline-secondary failures" :disabled="service.stats.failures === 0" :class="{'text-white btn-secondary': openTab==='failures'}">
Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span></button> Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span></button>
</div> </div>
<div class="col-3 pt-2">
<span class="text-black-50 float-right">{{service.online_7_days}}% Uptime</span>
</div>
<div v-if="openTab === 'incident'" class="col-12 mt-4"> <div v-if="openTab === 'incident'" class="col-12 mt-4">
<FormIncident :service="service" /> <FormIncident :service="service" />
</div> </div>
<div v-if="openTab === 'checkin'" class="col-12 mt-4">
<Checkin :service="service" />
</div>
<div v-if="openTab === 'failures'" class="col-12 mt-4"> <div v-if="openTab === 'failures'" class="col-12 mt-4">
<button @click.prevent="deleteFailures" class="btn btn-block btn-outline-secondary delete_failures" :disabled="service.stats.failures === 0">Delete Failures</button> <button @click.prevent="deleteFailures" class="btn btn-block btn-outline-secondary delete_failures" :disabled="service.stats.failures === 0">Delete Failures</button>
@ -66,8 +85,6 @@
</div> </div>
</div> </div>
</transition>
</div> </div>
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light"> <span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
@ -79,6 +96,7 @@
</template> </template>
<script> <script>
import Checkin from '../../forms/Checkin';
import FormIncident from '../../forms/Incident'; import FormIncident from '../../forms/Incident';
import FormMessage from '../../forms/Message'; import FormMessage from '../../forms/Message';
import ServiceFailures from './ServiceFailures'; import ServiceFailures from './ServiceFailures';
@ -89,6 +107,7 @@
export default { export default {
name: 'ServiceInfo', name: 'ServiceInfo',
components: { components: {
Checkin,
ServiceFailures, ServiceFailures,
FormIncident, FormIncident,
FormMessage, FormMessage,
@ -109,17 +128,27 @@
loaded: false, loaded: false,
set1_name: "", set1_name: "",
set2_name: "", set2_name: "",
failures: null failures: null,
visible: false
} }
}, },
async mounted() { watch: {
},
methods: {
async setVisible(isVisible, entry) {
if (isVisible && !this.visible) {
await this.loadInfo()
this.visible = true
}
},
async loadInfo() {
this.set1 = await this.getHits(24 * 7, "6h") this.set1 = await this.getHits(24 * 7, "6h")
this.set1_name = this.calc(this.set1) this.set1_name = this.calc(this.set1)
this.set2 = await this.getHits(24, "1h") this.set2 = await this.getHits(24, "1h")
this.set2_name = this.calc(this.set2) this.set2_name = this.calc(this.set2)
this.loaded = true this.loaded = true
}, },
methods: {
async deleteFailures() { async deleteFailures() {
const c = confirm('Are you sure you want to delete all failures?') const c = confirm('Are you sure you want to delete all failures?')
if (c) { if (c) {

View File

@ -1,23 +1,41 @@
<template> <template>
<form @submit.prevent="saveCheckin"> <div>
<div class="form-group row"> <div v-for="(checkin, i) in checkins" class="col-12 alert alert-light" role="alert">
<div class="col-md-3"> <span class="badge badge-pill badge-info text-uppercase">{{checkin.name}}</span>
<label for="checkin_interval" class="col-form-label">Checkin Name</label> <span class="float-right font-2">Last checkin {{ago(checkin.last_hit)}}</span>
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin"> <span class="float-right font-2 mr-3">Check Every {{checkin.interval}} seconds</span>
</div> <span class="float-right font-2 mr-3">Grace Period {{checkin.grace}} seconds</span>
<div class="col-3"> <span class="d-block mt-2">
<label for="checkin_interval" class="col-form-label">Interval (seconds)</label> <input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60"> <span class="small">Send a GET request to this URL every {{checkin.interval}} seconds
</div> <button @click="deleteCheckin(checkin)" type="button" class="btn btn-danger btn-xs float-right mt-1">Delete</button>
<div class="col-3"> </span>
<label for="grace_period" class="col-form-label">Grace Period</label> </span>
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10"> </div>
</div>
<div class="col-3"> <div class="col-12 alert alert-light">
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-success d-block" style="margin-top: 14px;">Save Checkin</button> <form @submit.prevent="saveCheckin">
</div> <div class="form-group row">
<div class="col-5">
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-2">
<label for="checkin_interval" class="col-form-label">Interval</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
</div>
<div class="col-2">
<label for="grace_period" class="col-form-label">Grace Period</label>
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
</div>
<div class="col-3">
<label class="col-form-label"></label>
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-success d-block mt-2">Save Checkin</button>
</div>
</div>
</form>
</div>
</div> </div>
</form>
</template> </template>
<script> <script>
@ -37,20 +55,41 @@
name: "", name: "",
interval: 60, interval: 60,
grace: 60, grace: 60,
service: this.service.id service_id: this.service.id
} }
} }
}, },
mounted() { mounted() {
}, },
methods: { computed: {
async saveCheckin() { checkins() {
const data = {name: this.group.name, public: this.group.public} return this.$store.getters.serviceCheckins(this.service.id)
await Api.group_create(data)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
}, },
core() {
return this.$store.getters.core
},
},
methods: {
fixInts() {
const c = this.checkin
this.checkin.interval = parseInt(c.interval)
this.checkin.grace = parseInt(c.grace)
return this.checkin
},
async saveCheckin() {
const c = this.fixInts()
await Api.checkin_create(c)
await this.updateCheckins()
},
async deleteCheckin(checkin) {
await Api.checkin_delete(checkin)
await this.updateCheckins()
},
async updateCheckins() {
const checkins = await Api.checkins()
this.$store.commit('setCheckins', checkins)
}
} }
} }
</script> </script>

View File

@ -31,7 +31,20 @@
<small class="form-text text-muted">HTML is allowed inside the footer</small> <small class="form-text text-muted">HTML is allowed inside the footer</small>
</div> </div>
<button @click.prevent="saveSettings" id="save_core" type="submit" class="btn btn-primary btn-block">Save Settings</button> <div class="form-group row mt-3">
<label class="col-sm-10 col-form-label">Enable Error Reporting</label>
<div class="col-sm-2 float-right">
<span @click="core.allow_reports = !!core.allow_reports" class="switch" id="allow_report">
<input v-model="core.allow_reports" type="checkbox" name="allow_report" class="switch" id="switch_allow_report" :checked="core.allow_reports">
<label for="switch_allow_report"></label>
</span>
</div>
<div class="col-12">
<small>Help the Statping project out by sending anonymous error logs back to our server.</small>
</div>
</div>
<button @click.prevent="saveSettings" id="save_core" type="submit" class="btn btn-primary btn-block mt-3">Save Settings</button>
</form> </form>
</template> </template>
@ -41,24 +54,17 @@
export default { export default {
name: 'CoreSettings', name: 'CoreSettings',
props: { computed: {
in_core: { core() {
type: Object, return this.$store.getters.core
required: true,
} }
}, },
data() {
return {
core: this.in_core
}
},
methods: { methods: {
async saveSettings() { async saveSettings() {
const c = this.core const c = this.core
await Api.core_save(c) await Api.core_save(c)
const core = await Api.core() const core = await Api.core()
this.$store.commit('setCore', core) this.$store.commit('setCore', core)
this.core = core
}, },
selectAll() { selectAll() {
this.$refs.input.select(); this.$refs.input.select();

View File

@ -1,80 +0,0 @@
<template>
<form @submit="updateCore">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Github Client ID</label>
<div class="col-sm-8">
<input v-model="clientId" type="text" class="form-control" placeholder="" required>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Github Client Secret</label>
<div class="col-sm-8">
<input v-model="clientSecret" type="text" class="form-control" placeholder="" required>
</div>
</div>
<div class="form-group row">
<label for="switch-group-public" class="col-sm-4 col-form-label">Enabled</label>
<div class="col-md-8 col-xs-12 mt-1">
<span @click="enabled = !!enabled" class="switch float-left">
<input v-model="enabled" type="checkbox" class="switch" id="switch-group-public" :checked="enabled">
<label for="switch-group-public">Enabled Github Auth</label>
</span>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button @click="updateCore" type="submit" :disabled="loading || group.name === ''" class="btn btn-block" :class="{'btn-primary': !group.id, 'btn-secondary': group.id}">
{{loading ? "Loading..." : group.id ? "Update Group" : "Create Group"}}
</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</template>
<script>
import Api from "../API";
export default {
name: 'FormGroup',
props: {
in_group: {
type: Object
},
edit: {
type: Function
}
},
data() {
return {
loading: false,
clientId: "",
clientSecret: "",
enabled: true,
}
},
watch: {
in_group() {
this.group = this.in_group
}
},
methods: {
removeEdit() {
this.group = {}
this.edit(false)
},
async updateCore() {
const g = this.group
const data = {id: g.id, name: g.name, public: g.public}
await Api.core_save(data)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
this.edit(false)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -80,17 +80,19 @@
const g = this.group const g = this.group
const data = {name: g.name, public: g.public} const data = {name: g.name, public: g.public}
await Api.group_create(data) await Api.group_create(data)
const groups = await Api.groups() await this.update()
this.$store.commit('setGroups', groups)
this.group = {} this.group = {}
}, },
async updateGroup() { async updateGroup() {
const g = this.group const g = this.group
const data = {id: g.id, name: g.name, public: g.public} const data = {id: g.id, name: g.name, public: g.public}
await Api.group_update(data) await Api.group_update(data)
await this.update()
this.edit(false)
},
async update() {
const groups = await Api.groups() const groups = await Api.groups()
this.$store.commit('setGroups', groups) this.$store.commit('setGroups', groups)
this.edit(false)
} }
} }
} }

View File

@ -8,11 +8,9 @@
</button> </button>
</div> </div>
<div class="card-body bg-light pt-3"> <div class="card-body bg-light pt-3">
<div v-for="(update, i) in incident.updates" class="alert alert-light" role="alert"> <div v-for="(update, i) in incident.updates" class="alert alert-light" role="alert">
<span class="badge badge-pill badge-info text-uppercase">{{update.type}}</span> <span class="badge badge-pill badge-info text-uppercase">{{update.type}}</span>
<span class="float-right font-2">{{ago(update.created_at)}} ago</span> <span class="float-right font-2">{{ago(update.created_at)}} ago</span>
<span class="d-block mt-2">{{update.message}} <span class="d-block mt-2">{{update.message}}
<button @click="delete_update(update)" type="button" class="close" data-dismiss="alert" aria-label="Close"> <button @click="delete_update(update)" type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
@ -95,9 +93,9 @@
await Api.incident_update_delete(update) await Api.incident_update_delete(update)
this.incidents = await Api.incidents_service(this.service) this.incidents = await Api.incidents_service(this.service)
}, },
async loadIncidents() { async loadIncidents() {
this.incidents = await Api.incidents_service(this.service) this.incidents = await Api.incidents_service(this.service)
}, },
async createIncident() { async createIncident() {
await Api.incident_create(this.service, this.incident) await Api.incident_create(this.service, this.incident)
await this.loadIncidents() await this.loadIncidents()
@ -107,13 +105,13 @@
service: this.service.id, service: this.service.id,
} }
}, },
async deleteIncident(incident) { async deleteIncident(incident) {
let c = confirm(`Are you sure you want to delete '${incident.title}'?`) let c = confirm(`Are you sure you want to delete '${incident.title}'?`)
if (c) { if (c) {
await Api.incident_delete(incident) await Api.incident_delete(incident)
await this.loadIncidents() await this.loadIncidents()
}
} }
}
} }
} }
</script> </script>

View File

@ -23,10 +23,6 @@
</div> </div>
</div> </div>
<a v-if="oauth.oauth_providers.split(',').includes('github')" class="btn btn-block btn-outline-dark" :href="`https://github.com/login/oauth/authorize?scope=user:email&client_id=${oauth.gh_client_id}`">Login with Github</a>
<a v-if="oauth.oauth_providers.split(',').includes('google')" class="btn btn-block btn-outline-secondary" :href="`https://accounts.google.com/signin/oauth?client_id=${oauth.google_client_id}&response_type=code&scope=${google_scope}&redirect_uri=${$store.getters.core.domain}/oauth/google`">Login with Google</a>
<a v-if="oauth.oauth_providers.split(',').includes('slack')" class="btn btn-block btn-outline-secondary" :href="`https://slack.com/oauth/v2/authorize?client_id=${oauth.slack_client_id}&team=${oauth.slack_team}&user_scope=${slack_scope}&redirect_uri=${$store.getters.core.domain}/oauth/slack`">Login with Slack</a>
</form> </form>
</template> </template>
@ -35,11 +31,14 @@
export default { export default {
name: 'FormLogin', name: 'FormLogin',
props: { computed: {
oauth: { core() {
type: Object return this.$store.getters.core
} },
}, oauth() {
return this.$store.getters.core.oauth
}
},
data() { data() {
return { return {
username: "", username: "",

View File

@ -35,20 +35,25 @@
</div> </div>
</div> </div>
<div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div> <div v-if="error && !success" class="alert alert-danger col-12" role="alert">
<div v-if="success" class="alert alert-success col-12" role="alert">{{notifier.title}} appears to be working!</div> {{error}}<p v-if="response">Response:<br>{{response}}</p>
</div>
<div v-if="success" class="alert alert-success col-12" role="alert">
{{notifier.title}} appears to be working!
<p v-if="response">Response:<br>{{response}}</p>
</div>
<div class="card text-black-50 bg-white mb-3"> <div class="card text-black-50 bg-white mb-3">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-6 col-sm-6 mb-2 mb-sm-0 mt-2 mt-sm-0"> <div class="col-6 col-sm-6 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click.prevent="saveNotifier" type="submit" class="btn btn-block text-capitalize btn-primary"> <button @click.prevent="saveNotifier" type="submit" class="btn btn-block text-capitalize btn-primary save-notifier">
<i class="fa fa-check-circle"></i> {{loading ? "Loading..." : saved ? "Saved" : "Save Settings"}} <i class="fa fa-check-circle"></i> {{loading ? "Loading..." : saved ? "Saved" : "Save Settings"}}
</button> </button>
</div> </div>
<div class="col-6 col-sm-6 mb-2 mb-sm-0 mt-2 mt-sm-0"> <div class="col-6 col-sm-6 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click.prevent="testNotifier" class="btn btn-outline-dark btn-block text-capitalize"><i class="fa fa-vial"></i> <button @click.prevent="testNotifier" class="btn btn-outline-dark btn-block text-capitalize test-notifier"><i class="fa fa-vial"></i>
{{loadingTest ? "Loading..." : "Test Notifier"}}</button> {{loadingTest ? "Loading..." : "Test Notifier"}}</button>
</div> </div>
</div> </div>
@ -79,6 +84,7 @@ export default {
loading: false, loading: false,
loadingTest: false, loadingTest: false,
error: null, error: null,
response: null,
success: false, success: false,
saved: false, saved: false,
form: {}, form: {},
@ -130,6 +136,7 @@ export default {
} else { } else {
this.error = tested.error this.error = tested.error
} }
this.response = tested.response
this.loadingTest = false this.loadingTest = false
}, },
} }

View File

@ -6,8 +6,8 @@
<div class="form-group row"> <div class="form-group row">
<label for="switch-gh-oauth" class="col-sm-4 col-form-label">OAuth Login Settings</label> <label for="switch-gh-oauth" class="col-sm-4 col-form-label">OAuth Login Settings</label>
<div class="col-md-8 col-xs-12 mt-1"> <div class="col-md-8 col-xs-12 mt-1">
<span @click="internal_enabled = !!internal_enabled" class="switch float-left"> <span @click="oauth.internal_enabled = !!core.oauth.internal_enabled" class="switch float-left">
<input v-model="internal_enabled" type="checkbox" class="switch" id="switch-local-oauth" :checked="internal_enabled"> <input v-model="oauth.internal_enabled" type="checkbox" class="switch" id="switch-local-oauth" :checked="oauth.internal_enabled">
<label for="switch-local-oauth">Use email/password Authentication</label> <label for="switch-local-oauth">Use email/password Authentication</label>
</span> </span>
</div> </div>
@ -15,7 +15,7 @@
<div class="form-group row"> <div class="form-group row">
<label for="whitelist_domains" class="col-sm-4 col-form-label">Whitelist Domains</label> <label for="whitelist_domains" class="col-sm-4 col-form-label">Whitelist Domains</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input v-model="oauth.oauth_domains" type="text" class="form-control" placeholder="domain.com" id="whitelist_domains"> <input v-model="oauth.oauth.oauth_domains" type="text" class="form-control" placeholder="domain.com" id="whitelist_domains">
</div> </div>
</div> </div>
</div> </div>
@ -28,20 +28,20 @@
<div class="form-group row mt-3"> <div class="form-group row mt-3">
<label for="github_client" class="col-sm-4 col-form-label">Github Client ID</label> <label for="github_client" class="col-sm-4 col-form-label">Github Client ID</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input v-model="oauth.gh_client_id" type="text" class="form-control" id="github_client" required> <input v-model="oauth.oauth.gh_client_id" type="text" class="form-control" id="github_client" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="github_secret" class="col-sm-4 col-form-label">Github Client Secret</label> <label for="github_secret" class="col-sm-4 col-form-label">Github Client Secret</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input v-model="oauth.gh_client_secret" type="text" class="form-control" id="github_secret" required> <input v-model="oauth.oauth.gh_client_secret" type="text" class="form-control" id="github_secret" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="switch-gh-oauth" class="col-sm-4 col-form-label">Enable Github Login</label> <label for="switch-gh-oauth" class="col-sm-4 col-form-label">Enable Github Login</label>
<div class="col-md-8 col-xs-12 mt-1"> <div class="col-md-8 col-xs-12 mt-1">
<span @click="github_enabled = !!github_enabled" class="switch float-left"> <span @click="oauth.github_enabled = !!oauth.github_enabled" class="switch float-left">
<input v-model="github_enabled" type="checkbox" class="switch" id="switch-gh-oauth" :checked="github_enabled"> <input v-model="oauth.github_enabled" type="checkbox" class="switch" id="switch-gh-oauth" :checked="oauth.github_enabled">
<label for="switch-gh-oauth"> </label> <label for="switch-gh-oauth"> </label>
</span> </span>
</div> </div>
@ -49,7 +49,12 @@
<div class="form-group row"> <div class="form-group row">
<label for="gh_callback" class="col-sm-4 col-form-label">Callback URL</label> <label for="gh_callback" class="col-sm-4 col-form-label">Callback URL</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input v-bind:value="`${$store.getters.core.domain}/oauth/github`" type="text" class="form-control" id="gh_callback" readonly> <div class="input-group">
<input v-bind:value="`${core.domain}/oauth/github`" type="text" class="form-control" id="gh_callback" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${core.domain}/oauth/github`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -81,9 +86,14 @@
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="callback" class="col-sm-4 col-form-label">Callback URL</label> <label for="google_callback" class="col-sm-4 col-form-label">Callback URL</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input v-bind:value="`${$store.getters.core.domain}/oauth/google`" type="text" class="form-control" id="callback" readonly> <div class="input-group">
<input v-bind:value="`${core.domain}/oauth/google`" type="text" class="form-control" id="google_callback" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${core.domain}/oauth/google`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -124,14 +134,17 @@
<div class="form-group row"> <div class="form-group row">
<label for="slack_callback" class="col-sm-4 col-form-label">Callback URL</label> <label for="slack_callback" class="col-sm-4 col-form-label">Callback URL</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input v-bind:value="`${$store.getters.core.domain}/oauth/slack`" type="text" class="form-control" id="slack_callback" readonly> <div class="input-group">
<input v-bind:value="`${core.domain}/oauth/slack`" type="text" class="form-control" id="slack_callback" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${core.domain}/oauth/slack`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{providers()}}
<button class="btn btn-primary btn-block" @click.prevent="saveOAuth" type="submit"> <button class="btn btn-primary btn-block" @click.prevent="saveOAuth" type="submit">
Save OAuth Settings Save OAuth Settings
</button> </button>
@ -144,25 +157,34 @@
export default { export default {
name: 'OAuth', name: 'OAuth',
props: { computed: {
oauth: { oauth() {
type: Object return this.$store.getters.core.oauth
}
},
data() {
return {
internal_enabled: this.$store.getters.core.oauth.oauth_providers.split(",").includes('local'),
google_enabled: this.$store.getters.core.oauth.oauth_providers.split(",").includes('google'),
github_enabled: this.$store.getters.core.oauth.oauth_providers.split(",").includes('github'),
slack_enabled: this.$store.getters.core.oauth.oauth_providers.split(",").includes('slack')
} }
}, },
data() {
return {
internal_enabled: this.has('local'),
google_enabled: this.has('google'),
github_enabled: this.has('github'),
slack_enabled: this.has('slack')
}
},
mounted() {
window.console.log(this.core.oauth)
},
beforeCreate() { beforeCreate() {
// this.github_enabled = this.$store.getters.core.oauth.oauth_providers.split(",").includes('github') // this.github_enabled = this.$store.getters.core.oauth.oauth_providers.split(",").includes('github')
// const c = await Api.core() // const c = await Api.core()
// this.auth = c.auth // this.auth = c.auth
}, },
methods: { methods: {
has(val) {
if (!this.core.oauth.oauth_providers) {
return false
}
return this.core.oauth.oauth_providers.split(",").includes(val)
},
providers() { providers() {
let providers = []; let providers = [];
if (this.github_enabled) { if (this.github_enabled) {
@ -186,7 +208,6 @@
await Api.core_save(c) await Api.core_save(c)
const core = await Api.core() const core = await Api.core()
this.$store.commit('setCore', core) this.$store.commit('setCore', core)
this.core = core
} }
} }
} }

View File

@ -5,84 +5,83 @@
</div> </div>
<div class="col-12"> <div class="col-12">
<form @submit.prevent="saveSetup">
<div class="row">
<div class="col-6">
<div class="form-group">
<label>Database Connection</label>
<select @change="canSubmit" v-model="setup.db_connection" id="db_connection" class="form-control">
<option value="sqlite">Sqlite</option>
<option value="postgres">Postgres</option>
<option value="mysql">MySQL</option>
</select>
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label>Host</label>
<input @keyup="canSubmit" v-model="setup.db_host" id="db_host" type="text" class="form-control" placeholder="localhost">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label>Database Port</label>
<input @keyup="canSubmit" v-model="setup.db_port" id="db_port" type="text" class="form-control" placeholder="localhost">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label>Username</label>
<input @keyup="canSubmit" v-model="setup.db_user" id="db_user" type="text" class="form-control" placeholder="root">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label for="db_password">Password</label>
<input @keyup="canSubmit" v-model="setup.db_password" id="db_password" type="password" class="form-control" placeholder="password123">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label for="db_database">Database</label>
<input @keyup="canSubmit" v-model="setup.db_database" id="db_database" type="text" class="form-control" placeholder="Database name">
</div>
<form @submit.prevent="saveSetup"> </div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label>Database Connection</label>
<select @change="canSubmit" v-model="setup.db_connection" id="db_connection" class="form-control">
<option value="sqlite">Sqlite</option>
<option value="postgres">Postgres</option>
<option value="mysql">MySQL</option>
</select>
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label>Host</label>
<input @keyup="canSubmit" v-model="setup.db_host" id="db_host" type="text" class="form-control" placeholder="localhost">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label>Database Port</label>
<input @keyup="canSubmit" v-model="setup.db_port" id="db_port" type="text" class="form-control" placeholder="localhost">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label>Username</label>
<input @keyup="canSubmit" v-model="setup.db_user" id="db_user" type="text" class="form-control" placeholder="root">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label for="db_password">Password</label>
<input @keyup="canSubmit" v-model="setup.db_password" id="db_password" type="password" class="form-control" placeholder="password123">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label for="db_database">Database</label>
<input @keyup="canSubmit" v-model="setup.db_database" id="db_database" type="text" class="form-control" placeholder="Database name">
</div>
</div> <div class="col-6">
<div class="col-6"> <div class="form-group">
<label>Project Name</label>
<input @keyup="canSubmit" v-model="setup.project" id="project" type="text" class="form-control" placeholder="Great Uptime" required>
</div>
<div class="form-group"> <div class="form-group">
<label>Project Name</label> <label>Project Description</label>
<input @keyup="canSubmit" v-model="setup.project" id="project" type="text" class="form-control" placeholder="Great Uptime" required> <input @keyup="canSubmit" v-model="setup.description" id="description" type="text" class="form-control" placeholder="Great Uptime">
</div>
<div class="form-group">
<label for="domain">Domain URL</label>
<input @keyup="canSubmit" v-model="setup.domain" type="text" class="form-control" id="domain" required>
</div>
<div class="form-group">
<label>Admin Username</label>
<input @keyup="canSubmit" v-model="setup.username" id="username" type="text" class="form-control" placeholder="admin" required>
</div>
<div class="form-group">
<label>Admin Password</label>
<input @keyup="canSubmit" v-model="setup.password" id="password" type="password" class="form-control" placeholder="password" required>
</div>
<div class="form-group">
<label>Confirm Admin Password</label>
<input @keyup="canSubmit" v-model="setup.confirm_password" id="password_confirm" type="password" class="form-control" placeholder="password" required>
</div>
</div>
<div v-if="error" class="col-12 alert alert-danger">
{{error}}
</div>
<button @click.prevent="saveSetup" v-bind:disabled="disabled || loading" type="submit" class="btn btn-primary btn-block" :class="{'btn-primary': !loading, 'btn-default': loading}">
{{loading ? "Loading..." : "Save Settings"}}
</button>
</div> </div>
</form>
<div class="form-group">
<label>Project Description</label>
<input @keyup="canSubmit" v-model="setup.description" id="description" type="text" class="form-control" placeholder="Great Uptime">
</div>
<div class="form-group">
<label for="domain">Domain URL</label>
<input @keyup="canSubmit" v-model="setup.domain" type="text" class="form-control" id="domain" required>
</div>
<div class="form-group">
<label>Admin Username</label>
<input @keyup="canSubmit" v-model="setup.username" id="username" type="text" class="form-control" placeholder="admin" required>
</div>
<div class="form-group">
<label>Admin Password</label>
<input @keyup="canSubmit" v-model="setup.password" id="password" type="password" class="form-control" placeholder="password" required>
</div>
<div class="form-group">
<label>Confirm Admin Password</label>
<input @keyup="canSubmit" v-model="setup.confirm_password" id="password_confirm" type="password" class="form-control" placeholder="password" required>
</div>
</div>
<div v-if="error" class="col-12 alert alert-danger">
{{error}}
</div>
<button @click.prevent="saveSetup" v-bind:disabled="disabled || loading" type="submit" class="btn btn-primary btn-block" :class="{'btn-primary': !loading, 'btn-default': loading}">
{{loading ? "Loading..." : "Save Settings"}}
</button>
</div>
</form>
</div> </div>
</div> </div>
@ -163,6 +162,7 @@
return return
} }
await this.$store.dispatch('loadCore')
await this.$store.dispatch('loadRequired') await this.$store.dispatch('loadRequired')
this.loading = false this.loading = false

View File

@ -13,9 +13,9 @@
<input v-model="user.username" type="text" class="form-control" id="username" placeholder="Username" required autocorrect="off" autocapitalize="none" v-bind:readonly="user.id"> <input v-model="user.username" type="text" class="form-control" id="username" placeholder="Username" required autocorrect="off" autocapitalize="none" v-bind:readonly="user.id">
</div> </div>
<div class="col-6 col-md-4"> <div class="col-6 col-md-4">
<span @click="user.admin = !!user.admin" class="switch"> <span id="admin_switch" @click="user.admin = !!user.admin" class="switch">
<input v-model="user.admin" type="checkbox" class="switch" id="switch-normal" v-bind:checked="user.admin"> <input v-model="user.admin" type="checkbox" class="switch" id="user_admin_switch" v-bind:checked="user.admin">
<label for="switch-normal">Administrator</label> <label for="user_admin_switch">Administrator</label>
</span> </span>
</div> </div>
</div> </div>
@ -39,11 +39,12 @@
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" @click="saveUser" <LoadButton
class="btn-primary"
:disabled="loading || !user.username || !user.email || !user.password || !user.confirm_password || (user.password !== user.confirm_password)" :disabled="loading || !user.username || !user.email || !user.password || !user.confirm_password || (user.password !== user.confirm_password)"
class="btn btn-block" :class="{'btn-primary': !user.id, 'btn-secondary': user.id}"> :action="saveUser"
{{loading ? "Loading..." : user.id ? "Update User" : "Create User"}} :label="user.id ? 'Update User' : 'Create User'"
</button> />
</div> </div>
</div> </div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div> <div class="alert alert-danger d-none" id="alerter" role="alert"></div>
@ -54,10 +55,12 @@
<script> <script>
import Api from "../API"; import Api from "../API";
import LoadButton from "@/components/Elements/LoadButton";
export default { export default {
name: 'FormUser', name: 'FormUser',
props: { components: {LoadButton},
props: {
in_user: { in_user: {
type: Object type: Object
}, },
@ -81,6 +84,7 @@
in_user() { in_user() {
let u = this.in_user let u = this.in_user
u.password = null u.password = null
u.password_confirm = null
this.user = u this.user = u
} }
}, },
@ -89,8 +93,7 @@
this.user = {} this.user = {}
this.edit(false) this.edit(false)
}, },
async saveUser(e) { async saveUser() {
e.preventDefault();
this.loading = true this.loading = true
if (this.user.id) { if (this.user.id) {
await this.updateUser() await this.updateUser()
@ -103,8 +106,7 @@
let user = this.user let user = this.user
delete user.confirm_password delete user.confirm_password
await Api.user_create(user) await Api.user_create(user)
const users = await Api.users() await this.update()
this.$store.commit('setUsers', users)
this.user = {} this.user = {}
}, },
async updateUser() { async updateUser() {
@ -114,9 +116,12 @@
} }
delete user.confirm_password delete user.confirm_password
await Api.user_update(user) await Api.user_update(user)
await this.update()
this.edit(false)
},
async update() {
const users = await Api.users() const users = await Api.users()
this.$store.commit('setUsers', users) this.$store.commit('setUsers', users)
this.edit(false)
} }
} }
} }

View File

@ -1,9 +1,10 @@
import {library} from '@fortawesome/fontawesome-svg-core' import {library} from '@fortawesome/fontawesome-svg-core'
import {fas} from '@fortawesome/fontawesome-free-solid'; import {fas} from '@fortawesome/fontawesome-free-solid';
import {fab} from '@fortawesome/free-brands-svg-icons'; import {fab} from '@fortawesome/free-brands-svg-icons';
import {far} from '@fortawesome/fontawesome-svg-core';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome' import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import Vue from "vue"; import Vue from "vue";
library.add(fas, fab) library.add(fas, fab)
Vue.component('font-awesome-icon', FontAwesomeIcon) Vue.component('font-awesome-icon', FontAwesomeIcon)

View File

@ -6,9 +6,7 @@ import VueClipboard from 'vue-clipboard2'
import App from '@/App.vue' import App from '@/App.vue'
import store from './store' import store from './store'
import * as Sentry from '@sentry/browser';
import * as Integrations from '@sentry/integrations';
const errorReporter = "https://bed4d75404924cb3a799e370733a1b64@sentry.statping.com/3"
import router from './routes' import router from './routes'
import "./mixin" import "./mixin"
import "./icons" import "./icons"
@ -19,12 +17,6 @@ Vue.use(VueClipboard);
Vue.use(VueRouter); Vue.use(VueRouter);
Vue.use(VueObserveVisibility); Vue.use(VueObserveVisibility);
Sentry.init({
dsn: errorReporter,
integrations: [new Integrations.Vue({Vue, attachProps: true})],
});
Vue.config.productionTip = false Vue.config.productionTip = false
new Vue({ new Vue({
router, router,

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light"> <div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<TopNav :admin="$store.state.admin"/> <TopNav :admin="admin"/>
<router-view :admin="$store.state.admin"/> <router-view :admin="admin"/>
</div> </div>
</template> </template>
@ -17,18 +17,23 @@
data () { data () {
return { return {
authenticated: false, authenticated: false,
loaded: false,
} }
}, },
async mounted() { computed: {
const core = await Api.core() admin() {
this.$store.commit('setAdmin', core.admin) return this.$store.getters.admin
this.$store.commit('setCore', core) },
}, user() {
async created() { return this.$store.getters.user
const core = await Api.core() }
this.$store.commit('setCore', core) },
mounted() {
// if (!this.user || !this.admin) {
// this.$router.push('/login')
// }
}
} }
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -3,7 +3,7 @@
<Header/> <Header/>
<div v-for="(service, i) in $store.getters.servicesNoGroup" class="col-12 full-col-12"> <div v-for="(service, i) in services_no_group" class="col-12 full-col-12">
<div class="list-group online_list mb-4"> <div class="list-group online_list mb-4">
<a class="service_li list-group-item list-group-item-action"> <a class="service_li list-group-item list-group-item-action">
<router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link> <router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link>
@ -16,16 +16,16 @@
</div> </div>
</div> </div>
<div v-for="(group, index) in $store.getters.groupsInOrder" v-bind:key="index"> <div v-for="(group, index) in groups" v-bind:key="index">
<Group :group=group /> <Group :group=group />
</div> </div>
<div v-for="(message, index) in $store.getters.messages" v-bind:key="index" v-if="inRange(message) && message.service === 0"> <div v-for="(message, index) in messages" v-bind:key="index" v-if="inRange(message) && message.service === 0">
<MessageBlock :message="message"/> <MessageBlock :message="message"/>
</div> </div>
<div class="col-12 full-col-12"> <div class="col-12 full-col-12">
<div v-for="(service, index) in $store.getters.servicesInOrder" :ref="service.id" v-bind:key="index"> <div v-for="(service, index) in services" :ref="service.id" v-bind:key="index">
<ServiceBlock :service=service /> <ServiceBlock :service=service />
</div> </div>
</div> </div>
@ -58,6 +58,20 @@ export default {
logged_in: false logged_in: false
} }
}, },
computed: {
messages() {
return this.$store.getters.messages
},
groups() {
return this.$store.getters.groupsInOrder
},
services() {
return this.$store.getters.servicesInOrder
},
services_no_group() {
return this.$store.getters.servicesNoGroup
}
},
async created() { async created() {
this.logged_in = this.loggedIn() this.logged_in = this.loggedIn()
}, },
@ -69,11 +83,6 @@ export default {
const start = this.isBetween(new Date(), message.start_on) const start = this.isBetween(new Date(), message.start_on)
const end = this.isBetween(message.end_on, new Date()) const end = this.isBetween(message.end_on, new Date())
return start && end return start && end
},
clickService(s) {
this.$nextTick(() => {
this.$refs.s.scrollTop = 0;
});
} }
} }
} }

View File

@ -2,11 +2,9 @@
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light"> <div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="col-10 offset-1 col-md-8 offset-md-2 mt-md-2"> <div class="col-10 offset-1 col-md-8 offset-md-2 mt-md-2">
<div class="col-12 col-md-8 offset-md-2 mb-4"> <div class="col-12 col-md-8 offset-md-2 mb-4">
<img alt="Statping Login" class="col-12 mt-5 mt-md-0" style="max-width:680px" src="banner.png"> <img alt="Statping Login" class="col-12 mt-5 mt-md-0" style="max-width:650px" src="banner.png">
</div> </div>
<FormLogin/>
<FormLogin :oauth="$store.getters.core.oauth"/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -12,7 +12,6 @@ import Api from "../API";
export default { export default {
name: 'Logs', name: 'Logs',
components: {},
data() { data() {
return { return {
logs: [], logs: [],

View File

@ -21,8 +21,8 @@
<h6 class="mt-4 text-muted">Notifiers</h6> <h6 class="mt-4 text-muted">Notifiers</h6>
<div id="notifiers_tabs"> <div id="notifiers_tabs">
<a v-for="(notifier, index) in $store.getters.notifiers" v-bind:key="`${notifier.method}_${index}`" @click.prevent="changeTab" class="nav-link text-capitalize" v-bind:class="{active: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}" v-bind:id="`v-pills-${notifier.method.toLowerCase()}-tab`" data-toggle="pill" v-bind:href="`#v-pills-${notifier.method.toLowerCase()}`" role="tab" v-bind:aria-controls="`v-pills-${notifier.method.toLowerCase()}`" aria-selected="false"> <a v-for="(notifier, index) in notifiers" v-bind:key="`${notifier.method}_${index}`" @click.prevent="changeTab" class="nav-link text-capitalize" v-bind:class="{active: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}" v-bind:id="`v-pills-${notifier.method.toLowerCase()}-tab`" data-toggle="pill" v-bind:href="`#v-pills-${notifier.method.toLowerCase()}`" role="tab" v-bind:aria-controls="`v-pills-${notifier.method.toLowerCase()}`" aria-selected="false">
<font-awesome-icon :icon="iconName(notifier.icon)" class="mr-2"/> {{notifier.method}} <font-awesome-icon :icon="iconName(notifier.icon)" class="mr-2"/> {{notifier.title}}
<span v-if="notifier.enabled" class="badge badge-pill float-right mt-1" :class="{'badge-success': !liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), 'badge-light': liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), 'text-dark': liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}">ON</span> <span v-if="notifier.enabled" class="badge badge-pill float-right mt-1" :class="{'badge-success': !liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), 'badge-light': liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), 'text-dark': liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}">ON</span>
</a> </a>
</div> </div>
@ -60,11 +60,10 @@
<div class="card text-black-50 bg-white"> <div class="card text-black-50 bg-white">
<div class="card-header">Statping Settings</div> <div class="card-header">Statping Settings</div>
<div class="card-body"> <div class="card-body">
<CoreSettings :in_core="core"/> <CoreSettings/>
</div> </div>
</div> </div>
<div class="card text-black-50 bg-white mt-3"> <div class="card text-black-50 bg-white mt-3">
<div class="card-header">API Settings</div> <div class="card-header">API Settings</div>
<div class="card-body"> <div class="card-body">
@ -73,7 +72,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input v-model="core.api_key" type="text" class="form-control" id="api_key" readonly> <input v-model="core.api_key" type="text" class="form-control" id="api_key" readonly>
<div class="input-group-append"> <div class="input-group-append copy-btn">
<button @click.prevent="copy(core.api_key)" class="btn btn-outline-secondary" type="button">Copy</button> <button @click.prevent="copy(core.api_key)" class="btn btn-outline-secondary" type="button">Copy</button>
</div> </div>
</div> </div>
@ -86,12 +85,12 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input v-model="core.api_secret" @focus="$event.target.select()" type="text" class="form-control select-input" id="api_secret" readonly> <input v-model="core.api_secret" @focus="$event.target.select()" type="text" class="form-control select-input" id="api_secret" readonly>
<div class="input-group-append"> <div class="input-group-append copy-btn">
<button @click="copy(core.api_secret)" class="btn btn-outline-secondary" type="button">Copy</button> <button @click="copy(core.api_secret)" class="btn btn-outline-secondary" type="button">Copy</button>
</div> </div>
</div> </div>
<small class="form-text text-muted">API Secret is used for read, create, update and delete routes</small> <small class="form-text text-muted">API Secret is used for read, create, update and delete routes</small>
<small class="form-text text-muted">You can <a href="#" @click="renewApiKeys">Regenerate API Keys</a> if you need to.</small> <small class="form-text text-muted">You can <a href="#" id="regenkeys" @click="renewApiKeys">Regenerate API Keys</a> if you need to.</small>
</div> </div>
</div> </div>
</div> </div>
@ -112,7 +111,7 @@
<div class="card text-black-50 bg-white mb-5"> <div class="card text-black-50 bg-white mb-5">
<div class="card-header">Theme Editor</div> <div class="card-header">Theme Editor</div>
<div class="card-body"> <div class="card-body">
<ThemeEditor :core="core"/> <ThemeEditor/>
</div> </div>
</div> </div>
</div> </div>
@ -127,10 +126,10 @@
</div> </div>
<div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-oauth-tab'), show: liClass('v-pills-oauth-tab')}" id="v-pills-oauth" role="tabpanel" aria-labelledby="v-pills-oauth-tab"> <div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-oauth-tab'), show: liClass('v-pills-oauth-tab')}" id="v-pills-oauth" role="tabpanel" aria-labelledby="v-pills-oauth-tab">
<OAuth :oauth="core.oauth"/> <OAuth/>
</div> </div>
<div v-for="(notifier, index) in $store.getters.notifiers" v-bind:key="`${notifier.title}_${index}`" class="tab-pane fade" v-bind:class="{active: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), show: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}" v-bind:id="`v-pills-${notifier.method.toLowerCase()}-tab`" role="tabpanel" v-bind:aria-labelledby="`v-pills-${notifier.method.toLowerCase()}-tab`"> <div v-for="(notifier, index) in notifiers" v-bind:key="`${notifier.method}_${index}`" class="tab-pane fade" v-bind:class="{active: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), show: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}" v-bind:id="`v-pills-${notifier.method.toLowerCase()}-tab`" role="tabpanel" v-bind:aria-labelledby="`v-pills-${notifier.method.toLowerCase()}-tab`">
<Notifier :notifier="notifier"/> <Notifier :notifier="notifier"/>
</div> </div>
@ -167,20 +166,24 @@
tab: "v-pills-home-tab", tab: "v-pills-home-tab",
qrcode: "", qrcode: "",
qrurl: "", qrurl: "",
core: this.$store.getters.core }
},
computed: {
core() {
return this.$store.getters.core
},
notifiers() {
return this.$store.getters.notifiers
} }
}, },
async mounted() { async mounted() {
this.cache = await Api.cache() this.cache = await Api.cache()
}, },
async created() { async created() {
const c = this.$store.state.core const c = this.core
this.qrurl = `statping://setup?domain=${c.domain}&api=${c.api_secret}` this.qrurl = `statping://setup?domain=${c.domain}&api=${c.api_secret}`
this.qrcode = "https://chart.googleapis.com/chart?chs=500x500&cht=qr&chl=" + encodeURI(this.qrurl) this.qrcode = "https://chart.googleapis.com/chart?chs=500x500&cht=qr&chl=" + encodeURI(this.qrurl)
}, },
async beforeMount() {
this.core = await Api.core()
},
methods: { methods: {
changeTab(e) { changeTab(e) {
this.tab = e.target.id this.tab = e.target.id

View File

@ -26,7 +26,9 @@ export default new Vuex.Store({
messages: [], messages: [],
users: [], users: [],
notifiers: [], notifiers: [],
admin: false checkins: [],
admin: false,
user: false
}, },
getters: { getters: {
hasAllData: state => state.hasAllData, hasAllData: state => state.hasAllData,
@ -39,8 +41,10 @@ export default new Vuex.Store({
incidents: state => state.incidents, incidents: state => state.incidents,
users: state => state.users, users: state => state.users,
notifiers: state => state.notifiers, notifiers: state => state.notifiers,
checkins: state => state.checkins,
isAdmin: state => state.admin, isAdmin: state => state.admin,
isUser: state => state.user,
servicesInOrder: state => state.services.sort((a, b) => a.order_id - b.order_id), servicesInOrder: state => state.services.sort((a, b) => a.order_id - b.order_id),
servicesNoGroup: state => state.services.filter(g => g.group_id === 0).sort((a, b) => a.order_id - b.order_id), servicesNoGroup: state => state.services.filter(g => g.group_id === 0).sort((a, b) => a.order_id - b.order_id),
@ -48,6 +52,9 @@ export default new Vuex.Store({
groupsClean: state => state.groups.filter(g => g.name !== '').sort((a, b) => a.order_id - b.order_id), 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), 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) => { serviceByAll: (state) => (element) => {
if (element % 1 === 0) { if (element % 1 === 0) {
return state.services.find(s => s.id == element) return state.services.find(s => s.id == element)
@ -91,6 +98,7 @@ export default new Vuex.Store({
state.hasPublicData = bool state.hasPublicData = bool
}, },
setCore (state, core) { setCore (state, core) {
window.console.log('GETTING CORE')
state.core = core state.core = core
}, },
setToken (state, token) { setToken (state, token) {
@ -99,6 +107,9 @@ export default new Vuex.Store({
setServices (state, services) { setServices (state, services) {
state.services = services state.services = services
}, },
setCheckins (state, checkins) {
state.checkins = checkins
},
setGroups (state, groups) { setGroups (state, groups) {
state.groups = groups state.groups = groups
}, },
@ -114,15 +125,23 @@ export default new Vuex.Store({
setAdmin (state, admin) { setAdmin (state, admin) {
state.admin = admin state.admin = admin
}, },
setUser (state, user) {
state.user = user
},
}, },
actions: { actions: {
async getAllServices(context) { async getAllServices(context) {
const services = await Api.services() const services = await Api.services()
context.commit("setServices", services); context.commit("setServices", services);
}, },
async loadCore(context) {
const core = await Api.core()
context.commit("setCore", core);
context.commit('setAdmin', core.admin)
context.commit('setCore', core)
context.commit('setUser', core.logged_in)
},
async loadRequired(context) { async loadRequired(context) {
const core = await Api.core()
context.commit("setCore", core);
const groups = await Api.groups() const groups = await Api.groups()
context.commit("setGroups", groups); context.commit("setGroups", groups);
const services = await Api.services() const services = await Api.services()
@ -130,23 +149,15 @@ export default new Vuex.Store({
const messages = await Api.messages() const messages = await Api.messages()
context.commit("setMessages", messages) context.commit("setMessages", messages)
context.commit("setHasPublicData", true) context.commit("setHasPublicData", true)
// if (core.logged_in) {
// const notifiers = await Api.notifiers()
// context.commit("setNotifiers", notifiers);
// const users = await Api.users()
// context.commit("setUsers", users);
// const integrations = await Api.integrations()
// context.commit("setIntegrations", integrations);
// }
window.console.log('finished loading required data') window.console.log('finished loading required data')
}, },
async loadAdmin(context) { async loadAdmin(context) {
const core = await Api.core()
context.commit("setCore", core);
const groups = await Api.groups() const groups = await Api.groups()
context.commit("setGroups", groups); context.commit("setGroups", groups);
const services = await Api.services() const services = await Api.services()
context.commit("setServices", services); context.commit("setServices", services);
const checkins = await Api.checkins()
context.commit("setCheckins", checkins);
const messages = await Api.messages() const messages = await Api.messages()
context.commit("setMessages", messages) context.commit("setMessages", messages)
context.commit("setHasPublicData", true) context.commit("setHasPublicData", true)

View File

@ -81,6 +81,8 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) {
} }
app.OAuth = c.OAuth app.OAuth = c.OAuth
app.UseCdn = null.NewNullBool(c.UseCdn.Bool) app.UseCdn = null.NewNullBool(c.UseCdn.Bool)
app.AllowReports = null.NewNullBool(c.AllowReports.Bool)
utils.SentryInit(nil, app.AllowReports.Bool)
err = app.Update() err = app.Update()
returnJson(core.App, w, r) returnJson(core.App, w, r)
} }

View File

@ -195,26 +195,6 @@ func TestMainApiRoutes(t *testing.T) {
} }
} }
//func TestExportSettings(t *testing.T) {
// data, err := ExportSettings()
// require.Nil(t, err)
// assert.Len(t, data, 50)
//
// var exportData ExportData
// err = json.Unmarshal(data, &exportData)
// require.Nil(t, err)
//
// assert.Len(t, exportData.Services, 4)
// assert.Len(t, exportData.Messages, 4)
// assert.Len(t, exportData.Checkins, 2)
// assert.Len(t, exportData.Groups, 1)
//
// assert.Equal(t, "Updated Core", exportData.Core.Name)
// assert.True(t, exportData.Core.Setup)
// assert.NotEmpty(t, exportData.Core.ApiKey)
// assert.NotEmpty(t, exportData.Core.ApiSecret)
//}
type HttpFuncTest func(*testing.T) error type HttpFuncTest func(*testing.T) error
// HTTPTest contains all the parameters for a HTTP Unit Test // HTTPTest contains all the parameters for a HTTP Unit Test

View File

@ -88,6 +88,8 @@ func (s Storage) List() map[string]Item {
//Get a cached content by key //Get a cached content by key
func (s Storage) Get(key string) []byte { func (s Storage) Get(key string) []byte {
s.mu.Lock()
defer s.mu.Unlock()
item := s.items[key] item := s.items[key]
if item.Expired() { if item.Expired() {
CacheStorage.Delete(key) CacheStorage.Delete(key)

View File

@ -40,8 +40,7 @@ func checkinCreateHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
checkin.ServiceId = service.Id checkin.ServiceId = service.Id
err = checkin.Create() if err := checkin.Create(); err != nil {
if err != nil {
sendErrorJson(err, w, r) sendErrorJson(err, w, r)
return return
} }
@ -52,7 +51,7 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
checkin, err := checkins.FindByAPI(vars["api"]) checkin, err := checkins.FindByAPI(vars["api"])
if err != nil { 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 return
} }
ip, _, _ := net.SplitHostPort(r.RemoteAddr) ip, _, _ := net.SplitHostPort(r.RemoteAddr)
@ -60,15 +59,17 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) {
hit := &checkins.CheckinHit{ hit := &checkins.CheckinHit{
Checkin: checkin.Id, Checkin: checkin.Id,
From: ip, From: ip,
CreatedAt: utils.Now().UTC(), CreatedAt: utils.Now(),
} }
log.Infof("Checking %s was requested", checkin.Name)
err = hit.Create() err = hit.Create()
if err != nil { if err != nil {
sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r) sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r)
return return
} }
checkin.Failing = false checkin.Failing = false
checkin.LastHitTime = utils.Now().UTC() checkin.LastHitTime = utils.Now()
sendJsonAction(hit.Id, "update", w, r) sendJsonAction(hit.Id, "update", w, r)
} }

View File

@ -16,7 +16,7 @@ func TestApiCheckinRoutes(t *testing.T) {
SecureRoute: true, SecureRoute: true,
}, { }, {
Name: "Statping Create Checkin", Name: "Statping Create Checkin",
URL: "/api/checkin", URL: "/api/checkins",
Method: "POST", Method: "POST",
Body: `{ Body: `{
"service_id": 2, "service_id": 2,

View File

@ -52,7 +52,7 @@ func TestGroupAPIRoutes(t *testing.T) {
URL: "/api/groups", URL: "/api/groups",
Method: "GET", Method: "GET",
ExpectedStatus: 200, ExpectedStatus: 200,
ResponseLen: 2, ResponseLen: 3,
BeforeTest: UnsetTestENV, BeforeTest: UnsetTestENV,
}, },
{ {

View File

@ -8,11 +8,6 @@ import (
"net/http" "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) { func apiServiceIncidentsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
incids := incidents.FindByService(utils.ToInt(vars["id"])) incids := incidents.FindByService(utils.ToInt(vars["id"]))

View File

@ -72,16 +72,18 @@ func testNotificationHandler(w http.ResponseWriter, r *http.Request) {
} }
notif := services.ReturnNotifier(notifer.Method) notif := services.ReturnNotifier(notifer.Method)
err = notif.OnTest() out, err := notif.OnTest()
resp := &notifierTestResp{ resp := &notifierTestResp{
Success: err == nil, Success: err == nil,
Error: err, Response: out,
Error: err,
} }
returnJson(resp, w, r) returnJson(resp, w, r)
} }
type notifierTestResp struct { type notifierTestResp struct {
Success bool `json:"success"` Success bool `json:"success"`
Error error `json:"error,omitempty"` Response string `json:"response,omitempty"`
Error error `json:"error,omitempty"`
} }

View File

@ -146,9 +146,9 @@ func Router() *mux.Router {
// API CHECKIN Routes // API CHECKIN Routes
api.Handle("/api/checkins", authenticated(apiAllCheckinsHandler, false)).Methods("GET") api.Handle("/api/checkins", authenticated(apiAllCheckinsHandler, false)).Methods("GET")
api.Handle("/api/checkin/{api}", authenticated(apiCheckinHandler, false)).Methods("GET") api.Handle("/api/checkins", authenticated(checkinCreateHandler, false)).Methods("POST")
api.Handle("/api/checkin", authenticated(checkinCreateHandler, false)).Methods("POST") api.Handle("/api/checkins/{api}", authenticated(apiCheckinHandler, false)).Methods("GET")
api.Handle("/api/checkin/{api}", authenticated(checkinDeleteHandler, false)).Methods("DELETE") api.Handle("/api/checkins/{api}", authenticated(checkinDeleteHandler, false)).Methods("DELETE")
r.Handle("/checkin/{api}", http.HandlerFunc(checkinHitHandler)) r.Handle("/checkin/{api}", http.HandlerFunc(checkinHitHandler))
// Static Files Routes // Static Files Routes

View File

@ -23,11 +23,11 @@ func TestApiServiceRoutes(t *testing.T) {
Method: "GET", Method: "GET",
ExpectedContains: []string{`"name":"Google"`}, ExpectedContains: []string{`"name":"Google"`},
ExpectedStatus: 200, ExpectedStatus: 200,
ResponseLen: 5, ResponseLen: 6,
BeforeTest: SetTestENV, BeforeTest: SetTestENV,
FuncTest: func(t *testing.T) error { FuncTest: func(t *testing.T) error {
count := len(services.Services()) count := len(services.Services())
if count != 5 { if count != 6 {
return errors.Errorf("incorrect services count: %d", count) return errors.Errorf("incorrect services count: %d", count)
} }
return nil return nil
@ -39,11 +39,11 @@ func TestApiServiceRoutes(t *testing.T) {
Method: "GET", Method: "GET",
ExpectedContains: []string{`"name":"Google"`}, ExpectedContains: []string{`"name":"Google"`},
ExpectedStatus: 200, ExpectedStatus: 200,
ResponseLen: 4, ResponseLen: 5,
BeforeTest: UnsetTestENV, BeforeTest: UnsetTestENV,
FuncTest: func(t *testing.T) error { FuncTest: func(t *testing.T) error {
count := len(services.Services()) count := len(services.Services())
if count != 5 { if count != 6 {
return errors.Errorf("incorrect services count: %d", count) return errors.Errorf("incorrect services count: %d", count)
} }
return nil return nil
@ -59,7 +59,7 @@ func TestApiServiceRoutes(t *testing.T) {
}, },
{ {
Name: "Statping Private Service 1", Name: "Statping Private Service 1",
URL: "/api/services/2", URL: "/api/services/6",
Method: "GET", Method: "GET",
ExpectedContains: []string{`"error":"not authenticated"`}, ExpectedContains: []string{`"error":"not authenticated"`},
ExpectedStatus: 200, ExpectedStatus: 200,
@ -105,21 +105,18 @@ func TestApiServiceRoutes(t *testing.T) {
Name: "Statping Service 1 Failure Data - 24 Hour", Name: "Statping Service 1 Failure Data - 24 Hour",
URL: "/api/services/1/failure_data" + startEndQuery + "&group=24h", URL: "/api/services/1/failure_data" + startEndQuery + "&group=24h",
Method: "GET", Method: "GET",
ResponseLen: 4,
ExpectedStatus: 200, ExpectedStatus: 200,
}, },
{ {
Name: "Statping Service 1 Failure Data - 12 Hour", Name: "Statping Service 1 Failure Data - 12 Hour",
URL: "/api/services/1/failure_data" + startEndQuery + "&group=12h", URL: "/api/services/1/failure_data" + startEndQuery + "&group=12h",
Method: "GET", Method: "GET",
ResponseLen: 7,
ExpectedStatus: 200, ExpectedStatus: 200,
}, },
{ {
Name: "Statping Service 1 Failure Data - 1 Hour", Name: "Statping Service 1 Failure Data - 1 Hour",
URL: "/api/services/1/failure_data" + startEndQuery + "&group=1h", URL: "/api/services/1/failure_data" + startEndQuery + "&group=1h",
Method: "GET", Method: "GET",
ResponseLen: 73,
ExpectedStatus: 200, ExpectedStatus: 200,
}, },
{ {
@ -140,7 +137,6 @@ func TestApiServiceRoutes(t *testing.T) {
Name: "Statping Service 1 Failure Data", Name: "Statping Service 1 Failure Data",
URL: "/api/services/1/failure_data" + startEndQuery, URL: "/api/services/1/failure_data" + startEndQuery,
Method: "GET", Method: "GET",
ResponseLen: 73,
ExpectedStatus: 200, ExpectedStatus: 200,
}, },
{ {
@ -176,7 +172,7 @@ func TestApiServiceRoutes(t *testing.T) {
ExpectedContains: []string{`"status":"success","type":"service","method":"create"`, `"public":false`, `"group_id":1`}, ExpectedContains: []string{`"status":"success","type":"service","method":"create"`, `"public":false`, `"group_id":1`},
FuncTest: func(t *testing.T) error { FuncTest: func(t *testing.T) error {
count := len(services.Services()) count := len(services.Services())
if count != 6 { if count != 7 {
return errors.Errorf("incorrect services count: %d", count) return errors.Errorf("incorrect services count: %d", count)
} }
return nil return nil
@ -238,7 +234,7 @@ func TestApiServiceRoutes(t *testing.T) {
ExpectedContains: []string{`"status":"success"`, `"method":"delete"`}, ExpectedContains: []string{`"status":"success"`, `"method":"delete"`},
FuncTest: func(t *testing.T) error { FuncTest: func(t *testing.T) error {
count := len(services.Services()) count := len(services.Services())
if count != 5 { if count != 6 {
return errors.Errorf("incorrect services count: %d", count) return errors.Errorf("incorrect services count: %d", count)
} }
return nil return nil

View File

@ -22,7 +22,7 @@ func (c *commandLine) Select() *notifications.Notification {
var Command = &commandLine{&notifications.Notification{ var Command = &commandLine{&notifications.Notification{
Method: "command", Method: "command",
Title: "Shell Command", Title: "Command",
Description: "Shell Command allows you to run a customized shell/bash Command on the local machine it's running on.", Description: "Shell Command allows you to run a customized shell/bash Command on the local machine it's running on.",
Author: "Hunter Long", Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong", AuthorUrl: "https://github.com/hunterlong",
@ -33,37 +33,30 @@ var Command = &commandLine{&notifications.Notification{
Form: []notifications.NotificationForm{{ Form: []notifications.NotificationForm{{
Type: "text", Type: "text",
Title: "Shell or Bash", Title: "Shell or Bash",
Placeholder: "/bin/bash", Placeholder: "/usr/bin/curl",
DbField: "host", DbField: "host",
SmallText: "You can use '/bin/sh', '/bin/bash' or even an absolute path for an application.", SmallText: "You can use '/bin/sh', '/bin/bash', '/usr/bin/curl' or an absolute path for an application.",
}, { }, {
Type: "text", Type: "text",
Title: "Command to Run on OnSuccess", Title: "Command to Run on OnSuccess",
Placeholder: "curl google.com", Placeholder: "http://localhost:8080/health",
DbField: "var1", DbField: "var1",
SmallText: "This Command will run every time a service is receiving a Successful event.", SmallText: "This Command will run when a service is receiving a Successful event.",
}, { }, {
Type: "text", Type: "text",
Title: "Command to Run on OnFailure", Title: "Command to Run on OnFailure",
Placeholder: "curl offline.com", Placeholder: "http://localhost:8080/health",
DbField: "var2", DbField: "var2",
SmallText: "This Command will run every time a service is receiving a Failing event.", SmallText: "This Command will run when a service is receiving a Failing event.",
}}}, }}},
} }
func runCommand(app string, cmd ...string) (string, string, error) { func runCommand(app string, cmd ...string) (string, string, error) {
utils.Log.Infof("Command notifier sending: %s %s", app, strings.Join(cmd, " "))
outStr, errStr, err := utils.Command(app, cmd...) outStr, errStr, err := utils.Command(app, cmd...)
return outStr, errStr, err return outStr, errStr, err
} }
// OnFailure for commandLine will trigger failing service
func (c *commandLine) OnFailure(s *services.Service, f *failures.Failure) error {
msg := c.GetValue("var2")
tmpl := ReplaceVars(msg, s, f)
_, _, err := runCommand(c.Host, tmpl)
return err
}
// OnSuccess for commandLine will trigger successful service // OnSuccess for commandLine will trigger successful service
func (c *commandLine) OnSuccess(s *services.Service) error { func (c *commandLine) OnSuccess(s *services.Service) error {
msg := c.GetValue("var1") msg := c.GetValue("var1")
@ -72,11 +65,19 @@ func (c *commandLine) OnSuccess(s *services.Service) error {
return err return err
} }
// OnTest for commandLine triggers when this notifier has been saved // OnFailure for commandLine will trigger failing service
func (c *commandLine) OnTest() error { func (c *commandLine) OnFailure(s *services.Service, f *failures.Failure) error {
cmds := strings.Split(c.Var1, " ") msg := c.GetValue("var2")
in, out, err := runCommand(c.Host, cmds...) tmpl := ReplaceVars(msg, s, f)
utils.Log.Infoln(in) _, _, err := runCommand(c.Host, tmpl)
utils.Log.Infoln(out)
return err return err
} }
// OnTest for commandLine triggers when this notifier has been saved
func (c *commandLine) OnTest() (string, error) {
tmpl := ReplaceVars(c.Var1, exampleService, exampleFailure)
in, out, err := runCommand(c.Host, tmpl)
utils.Log.Infoln(in)
utils.Log.Infoln(out)
return out, err
}

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"github.com/statping/statping/types/failures" "github.com/statping/statping/types/failures"
"github.com/statping/statping/types/notifications" "github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/notifier" "github.com/statping/statping/types/notifier"
@ -61,23 +60,22 @@ func (d *discord) OnSuccess(s *services.Service) error {
} }
// OnSave triggers when this notifier has been saved // OnSave triggers when this notifier has been saved
func (d *discord) OnTest() error { func (d *discord) OnTest() (string, error) {
outError := errors.New("Incorrect discord URL, please confirm URL is correct") outError := errors.New("Incorrect discord URL, please confirm URL is correct")
message := `{"content": "Testing the discord notifier"}` message := `{"content": "Testing the discord notifier"}`
contents, _, err := utils.HttpRequest(Discorder.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(message)), time.Duration(10*time.Second), true) contents, _, err := utils.HttpRequest(Discorder.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(message)), time.Duration(10*time.Second), true)
if string(contents) == "" { if string(contents) == "" {
return nil return "", nil
} }
var dtt discordTestJson var dtt discordTestJson
err = json.Unmarshal(contents, &dtt) err = json.Unmarshal(contents, &dtt)
if err != nil { if err != nil {
return outError return string(contents), outError
} }
if dtt.Code == 0 { if dtt.Code == 0 {
return outError return string(contents), outError
} }
fmt.Println("discord: ", string(contents)) return string(contents), nil
return nil
} }
type discordTestJson struct { type discordTestJson struct {

View File

@ -186,7 +186,7 @@ func (e *emailer) OnSuccess(s *services.Service) error {
} }
// OnTest triggers when this notifier has been saved // OnTest triggers when this notifier has been saved
func (e *emailer) OnTest() error { func (e *emailer) OnTest() (string, error) {
testService := &services.Service{ testService := &services.Service{
Id: 1, Id: 1,
Name: "Example Service", Name: "Example Service",
@ -201,14 +201,16 @@ func (e *emailer) OnTest() error {
LastResponse: "<html>this is an example response</html>", LastResponse: "<html>this is an example response</html>",
CreatedAt: utils.Now().Add(-24 * time.Hour), CreatedAt: utils.Now().Add(-24 * time.Hour),
} }
subject := fmt.Sprintf("Service %v is Back Online", testService.Name)
email := &emailOutgoing{ email := &emailOutgoing{
To: e.Var2, To: e.Var2,
Subject: fmt.Sprintf("Service %v is Back Online", testService.Name), Subject: subject,
Template: mainEmailTemplate, Template: mainEmailTemplate,
Data: testService, Data: testService,
From: e.Var1, From: e.Var1,
} }
return e.dialSend(email) err := e.dialSend(email)
return subject, err
} }
func (e *emailer) dialSend(email *emailOutgoing) error { func (e *emailer) dialSend(email *emailOutgoing) error {

View File

@ -43,28 +43,31 @@ var LineNotify = &lineNotifier{&notifications.Notification{
} }
// Send will send a HTTP Post with the Authorization to the notify-api.line.me server. It accepts type: string // Send will send a HTTP Post with the Authorization to the notify-api.line.me server. It accepts type: string
func (l *lineNotifier) sendMessage(message string) error { func (l *lineNotifier) sendMessage(message string) (string, error) {
v := url.Values{} v := url.Values{}
v.Set("message", message) v.Set("message", message)
headers := []string{fmt.Sprintf("Authorization=Bearer %v", l.ApiSecret)} headers := []string{fmt.Sprintf("Authorization=Bearer %v", l.ApiSecret)}
_, _, err := utils.HttpRequest("https://notify-api.line.me/api/notify", "POST", "application/x-www-form-urlencoded", headers, strings.NewReader(v.Encode()), time.Duration(10*time.Second), true) content, _, err := utils.HttpRequest("https://notify-api.line.me/api/notify", "POST", "application/x-www-form-urlencoded", headers, strings.NewReader(v.Encode()), time.Duration(10*time.Second), true)
return err return string(content), err
} }
// OnFailure will trigger failing service // OnFailure will trigger failing service
func (l *lineNotifier) OnFailure(s *services.Service, f *failures.Failure) error { func (l *lineNotifier) OnFailure(s *services.Service, f *failures.Failure) error {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name) msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
return l.sendMessage(msg) _, err := l.sendMessage(msg)
return err
} }
// OnSuccess will trigger successful service // OnSuccess will trigger successful service
func (l *lineNotifier) OnSuccess(s *services.Service) error { func (l *lineNotifier) OnSuccess(s *services.Service) error {
msg := fmt.Sprintf("Service %s is online!", s.Name) msg := fmt.Sprintf("Service %s is online!", s.Name)
return l.sendMessage(msg) _, err := l.sendMessage(msg)
return err
} }
// OnTest triggers when this notifier has been saved // OnTest triggers when this notifier has been saved
func (l *lineNotifier) OnTest() error { func (l *lineNotifier) OnTest() (string, error) {
msg := fmt.Sprintf("Testing if Line Notifier is working!") msg := fmt.Sprintf("Testing if Line Notifier is working!")
return l.sendMessage(msg) _, err := l.sendMessage(msg)
return msg, err
} }

View File

@ -26,7 +26,7 @@ func (m *mobilePush) Select() *notifications.Notification {
var Mobile = &mobilePush{&notifications.Notification{ var Mobile = &mobilePush{&notifications.Notification{
Method: "mobile", Method: "mobile",
Title: "Mobile Notifications", Title: "Mobile",
Description: `Receive push notifications on your Mobile device using the Statping App. You can scan the Authentication QR Code found in Settings to get the Mobile app setup in seconds. Description: `Receive push notifications on your Mobile device using the Statping App. You can scan the Authentication QR Code found in Settings to get the Mobile app setup in seconds.
<p align="center"><a href="https://play.google.com/store/apps/details?id=com.statping"><img src="https://img.cjx.io/google-play.svg"></a><a href="https://itunes.apple.com/us/app/apple-store/id1445513219"><img src="https://img.cjx.io/app-store-badge.svg"></a></p>`, <p align="center"><a href="https://play.google.com/store/apps/details?id=com.statping"><img src="https://img.cjx.io/google-play.svg"></a><a href="https://itunes.apple.com/us/app/apple-store/id1445513219"><img src="https://img.cjx.io/app-store-badge.svg"></a></p>`,
Author: "Hunter Long", Author: "Hunter Long",
@ -97,7 +97,7 @@ func (m *mobilePush) OnSuccess(s *services.Service) error {
} }
// OnTest triggers when this notifier has been saved // OnTest triggers when this notifier has been saved
func (m *mobilePush) OnTest() error { func (m *mobilePush) OnTest() (string, error) {
msg := &pushArray{ msg := &pushArray{
Message: "Testing the Mobile Notifier", Message: "Testing the Mobile Notifier",
Title: "Testing Notifications", Title: "Testing Notifications",
@ -107,18 +107,18 @@ func (m *mobilePush) OnTest() error {
} }
body, err := pushRequest(msg) body, err := pushRequest(msg)
if err != nil { if err != nil {
return err return "", err
} }
var output mobileResponse var output mobileResponse
err = json.Unmarshal(body, &output) err = json.Unmarshal(body, &output)
if err != nil { if err != nil {
return err return string(body), err
} }
if len(output.Logs) == 0 { if len(output.Logs) == 0 {
return nil return string(body), err
} else { } else {
firstLog := output.Logs[0].Error firstLog := output.Logs[0].Error
return fmt.Errorf("Mobile Notification error: %v", firstLog) return string(body), fmt.Errorf("Mobile Notification error: %v", firstLog)
} }
} }

View File

@ -20,6 +20,7 @@ func InitNotifiers() {
Twilio, Twilio,
Webhook, Webhook,
Mobile, Mobile,
Pushover,
) )
} }

View File

@ -0,0 +1,85 @@
package notifiers
import (
"github.com/statping/statping/types/services"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
func TestAllNotifiers(t *testing.T) {
notifiers := []notifierTest{
{
Notifier: Command,
RequiredENV: nil,
},
{
Notifier: Discorder,
RequiredENV: []string{"DISCORD_URL"},
},
{
Notifier: email,
RequiredENV: []string{"EMAIL_HOST", "EMAIL_USER", "EMAIL_PASS", "EMAIL_OUTGOING", "EMAIL_SEND_TO", "EMAIL_PORT"},
},
{
Notifier: Mobile,
RequiredENV: []string{"MOBILE_ID", "MOBILE_NUMBER"},
},
{
Notifier: Pushover,
RequiredENV: []string{"PUSHOVER_TOKEN", "PUSHOVER_API"},
},
{
Notifier: slacker,
RequiredENV: []string{"SLACK_URL"},
},
{
Notifier: Telegram,
RequiredENV: []string{"TELEGRAM_TOKEN", "TELEGRAM_CHANNEL"},
},
{
Notifier: Twilio,
RequiredENV: []string{"TWILIO_SID", "TWILIO_SECRET", "TWILIO_FROM", "TWILIO_TO"},
},
{
Notifier: Webhook,
RequiredENV: nil,
},
}
for _, n := range notifiers {
if !getEnvs(n.RequiredENV) {
t.Skip()
continue
}
Add(n.Notifier)
err := n.Notifier.OnSuccess(exampleService)
assert.Nil(t, err)
err = n.Notifier.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
err = n.Notifier.OnTest()
assert.Nil(t, err)
}
}
func getEnvs(env []string) bool {
for _, v := range env {
if os.Getenv(v) == "" {
return false
}
}
return true
}
type notifierTest struct {
Notifier services.ServiceNotifier
RequiredENV []string
}

88
notifiers/pushover.go Normal file
View File

@ -0,0 +1,88 @@
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{&notifications.Notification{
Method: "pushover",
Title: "Pushover",
Description: "Use Pushover to receive push notifications. You will need to create a <a href=\"https://pushover.net/apps/build\">New Application</a> 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) (string, error) {
v := url.Values{}
v.Set("token", t.ApiSecret)
v.Set("user", t.ApiKey)
v.Set("message", message)
rb := strings.NewReader(v.Encode())
content, _, err := utils.HttpRequest(pushoverUrl, "POST", "application/x-www-form-urlencoded", nil, rb, time.Duration(10*time.Second), true)
if err != nil {
return "", err
}
return string(content), 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)
_, err := t.sendMessage(msg)
return err
}
// OnSuccess will trigger successful service
func (t *pushover) OnSuccess(s *services.Service) error {
msg := fmt.Sprintf("Your service '%v' is currently online!", s.Name)
_, err := t.sendMessage(msg)
return err
}
// OnTest will test the Pushover SMS messaging
func (t *pushover) OnTest() (string, error) {
msg := fmt.Sprintf("Testing the Pushover Notifier")
content, err := t.sendMessage(msg)
return content, err
}

View File

@ -0,0 +1,61 @@
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) {
t.SkipNow()
db, err := database.OpenTester()
require.Nil(t, err)
db.AutoMigrate(&notifications.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)
})
}

View File

@ -58,16 +58,16 @@ func (s *slack) sendSlack(msg string) error {
return nil return nil
} }
func (s *slack) OnTest() error { func (s *slack) OnTest() (string, error) {
contents, resp, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(`{"text":"testing message"}`)), time.Duration(10*time.Second), true) contents, resp, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(`{"text":"testing message"}`)), time.Duration(10*time.Second), true)
if err != nil { if err != nil {
return err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
if string(contents) != "ok" { if string(contents) != "ok" {
return errors.New("the slack response was incorrect, check the URL") return string(contents), errors.New("the slack response was incorrect, check the URL")
} }
return nil return string(contents), nil
} }
// OnFailure will trigger failing service // OnFailure will trigger failing service

View File

@ -51,7 +51,7 @@ var Telegram = &telegram{&notifications.Notification{
} }
// Send will send a HTTP Post to the Telegram API. It accepts type: string // Send will send a HTTP Post to the Telegram API. It accepts type: string
func (t *telegram) sendMessage(message string) error { func (t *telegram) sendMessage(message string) (string, error) {
apiEndpoint := fmt.Sprintf("https://api.telegram.org/bot%v/sendMessage", t.ApiSecret) apiEndpoint := fmt.Sprintf("https://api.telegram.org/bot%v/sendMessage", t.ApiSecret)
v := url.Values{} v := url.Values{}
@ -65,27 +65,30 @@ func (t *telegram) sendMessage(message string) error {
if !success { if !success {
errorOut := telegramError(contents) errorOut := telegramError(contents)
out := fmt.Sprintf("Error code %v - %v", errorOut.ErrorCode, errorOut.Description) out := fmt.Sprintf("Error code %v - %v", errorOut.ErrorCode, errorOut.Description)
return errors.New(out) return string(contents), errors.New(out)
} }
return err return string(contents), err
} }
// OnFailure will trigger failing service // OnFailure will trigger failing service
func (t *telegram) OnFailure(s *services.Service, f *failures.Failure) error { func (t *telegram) OnFailure(s *services.Service, f *failures.Failure) error {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name) msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
return t.sendMessage(msg) _, err := t.sendMessage(msg)
return err
} }
// OnSuccess will trigger successful service // OnSuccess will trigger successful service
func (t *telegram) OnSuccess(s *services.Service) error { func (t *telegram) OnSuccess(s *services.Service) error {
msg := fmt.Sprintf("Your service '%v' is currently online!", s.Name) msg := fmt.Sprintf("Your service '%v' is currently online!", s.Name)
return t.sendMessage(msg) _, err := t.sendMessage(msg)
return err
} }
// OnTest will test the Twilio SMS messaging // OnTest will test the Twilio SMS messaging
func (t *telegram) OnTest() error { func (t *telegram) OnTest() (string, error) {
msg := fmt.Sprintf("Testing the Twilio SMS Notifier on your Statping server") msg := fmt.Sprintf("Testing the Twilio SMS Notifier on your Statping server")
return t.sendMessage(msg) content, err := t.sendMessage(msg)
return content, err
} }
func telegramSuccess(res []byte) (bool, telegramResponse) { func telegramSuccess(res []byte) (bool, telegramResponse) {

View File

@ -25,6 +25,7 @@ func init() {
} }
func TestTelegramNotifier(t *testing.T) { func TestTelegramNotifier(t *testing.T) {
t.SkipNow()
db, err := database.OpenTester() db, err := database.OpenTester()
require.Nil(t, err) require.Nil(t, err)
db.AutoMigrate(&notifications.Notification{}) db.AutoMigrate(&notifications.Notification{})

View File

@ -61,7 +61,7 @@ var Twilio = &twilio{&notifications.Notification{
} }
// Send will send a HTTP Post to the Twilio SMS API. It accepts type: string // Send will send a HTTP Post to the Twilio SMS API. It accepts type: string
func (t *twilio) sendMessage(message string) error { func (t *twilio) sendMessage(message string) (string, error) {
twilioUrl := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%v/Messages.json", t.GetValue("api_key")) twilioUrl := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%v/Messages.json", t.GetValue("api_key"))
v := url.Values{} v := url.Values{}
@ -75,25 +75,27 @@ func (t *twilio) sendMessage(message string) error {
if !success { if !success {
errorOut := twilioError(contents) errorOut := twilioError(contents)
out := fmt.Sprintf("Error code %v - %v", errorOut.Code, errorOut.Message) out := fmt.Sprintf("Error code %v - %v", errorOut.Code, errorOut.Message)
return errors.New(out) return string(contents), errors.New(out)
} }
return err return string(contents), err
} }
// OnFailure will trigger failing service // OnFailure will trigger failing service
func (t *twilio) OnFailure(s *services.Service, f *failures.Failure) error { func (t *twilio) OnFailure(s *services.Service, f *failures.Failure) error {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name) msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
return t.sendMessage(msg) _, err := t.sendMessage(msg)
return err
} }
// OnSuccess will trigger successful service // OnSuccess will trigger successful service
func (t *twilio) OnSuccess(s *services.Service) error { func (t *twilio) OnSuccess(s *services.Service) error {
msg := fmt.Sprintf("Your service '%v' is currently online!", s.Name) msg := fmt.Sprintf("Your service '%v' is currently online!", s.Name)
return t.sendMessage(msg) _, err := t.sendMessage(msg)
return err
} }
// OnTest will test the Twilio SMS messaging // OnTest will test the Twilio SMS messaging
func (t *twilio) OnTest() error { func (t *twilio) OnTest() (string, error) {
msg := fmt.Sprintf("Testing the Twilio SMS Notifier") msg := fmt.Sprintf("Testing the Twilio SMS Notifier")
return t.sendMessage(msg) return t.sendMessage(msg)
} }

View File

@ -26,7 +26,7 @@ type webhooker struct {
var Webhook = &webhooker{&notifications.Notification{ var Webhook = &webhooker{&notifications.Notification{
Method: webhookMethod, Method: webhookMethod,
Title: "HTTP webhooker", Title: "Webhook",
Description: "Send a custom HTTP request to a specific URL with your own body, headers, and parameters.", Description: "Send a custom HTTP request to a specific URL with your own body, headers, and parameters.",
Author: "Hunter Long", Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong", AuthorUrl: "https://github.com/hunterlong",
@ -113,16 +113,17 @@ func (w *webhooker) sendHttpWebhook(body string) (*http.Response, error) {
return resp, err return resp, err
} }
func (w *webhooker) OnTest() error { func (w *webhooker) OnTest() (string, error) {
body := ReplaceVars(w.Var2, exampleService, exampleFailure) body := ReplaceVars(w.Var2, exampleService, exampleFailure)
resp, err := w.sendHttpWebhook(body) resp, err := w.sendHttpWebhook(body)
if err != nil { if err != nil {
return err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body) content, err := ioutil.ReadAll(resp.Body)
utils.Log.Infoln(fmt.Sprintf("Webhook notifier received: '%v'", string(content))) out := fmt.Sprintf("Webhook notifier received: '%v'", string(content))
return err utils.Log.Infoln(out)
return out, err
} }
// OnFailure will trigger failing service // OnFailure will trigger failing service

View File

@ -32,7 +32,7 @@ func All() []*Checkin {
} }
func (c *Checkin) Create() error { func (c *Checkin) Create() error {
c.ApiKey = utils.RandomString(7) c.ApiKey = utils.RandomString(32)
q := db.Create(c) q := db.Create(c)
c.Start() c.Start()

View File

@ -57,7 +57,7 @@ func Connect(configs *DbConfig, retry bool) error {
if err != nil { if err != nil {
log.Debugln(fmt.Sprintf("Database connection error %s", err)) log.Debugln(fmt.Sprintf("Database connection error %s", err))
if retry { 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) time.Sleep(5 * time.Second)
return Connect(configs, retry) return Connect(configs, retry)
} else { } else {

View File

@ -30,7 +30,13 @@ func Select() (*Core, error) {
return nil, db.Error() return nil, db.Error()
} }
App = &c App = &c
App.UseCdn = null.NewNullBool(os.Getenv("USE_CDN") == "true")
if os.Getenv("USE_CDN") == "true" {
App.UseCdn = null.NewNullBool(true)
}
if os.Getenv("ALLOW_REPORTS") == "true" {
App.AllowReports = null.NewNullBool(true)
}
return App, q.Error() return App, q.Error()
} }

View File

@ -35,6 +35,7 @@ type Core struct {
Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"` Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"`
LoggedIn bool `gorm:"-" json:"logged_in"` LoggedIn bool `gorm:"-" json:"logged_in"`
IsAdmin bool `gorm:"-" json:"admin"` IsAdmin bool `gorm:"-" json:"admin"`
AllowReports null.NullBool `gorm:"column:allow_reports;default:false" json:"allow_reports"`
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"`
Started time.Time `gorm:"-" json:"started_on"` Started time.Time `gorm:"-" json:"started_on"`

View File

@ -9,7 +9,7 @@ func Samples() error {
incident1 := &Incident{ incident1 := &Incident{
Title: "Github Issues", Title: "Github Issues",
Description: "There are new features for Statping, if you have any issues please visit the Github Repo.", Description: "There are new features for Statping, if you have any issues please visit the Github Repo.",
ServiceId: 2, ServiceId: 4,
} }
if err := incident1.Create(); err != nil { if err := incident1.Create(); err != nil {
return err return err
@ -18,7 +18,7 @@ func Samples() error {
incident2 := &Incident{ incident2 := &Incident{
Title: "Recent Downtime", Title: "Recent Downtime",
Description: "We've noticed an issue with authentications and we're looking into it now.", Description: "We've noticed an issue with authentications and we're looking into it now.",
ServiceId: 4, ServiceId: 5,
} }
if err := incident2.Create(); err != nil { if err := incident2.Create(); err != nil {
return err return err

View File

@ -9,5 +9,5 @@ import (
type Notifier interface { type Notifier interface {
OnSuccess(*services.Service) error // OnSuccess is triggered when a service is successful OnSuccess(*services.Service) error // OnSuccess is triggered when a service is successful
OnFailure(*services.Service, *failures.Failure) error // OnFailure is triggered when a service is failing OnFailure(*services.Service, *failures.Failure) error // OnFailure is triggered when a service is failing
OnTest() error // OnTest is triggered for testing OnTest() (string, error) // OnTest is triggered for testing
} }

View File

@ -28,6 +28,6 @@ func FindNotifier(method string) *notifications.Notification {
type ServiceNotifier interface { type ServiceNotifier interface {
OnSuccess(*Service) error // OnSuccess is triggered when a service is successful OnSuccess(*Service) error // OnSuccess is triggered when a service is successful
OnFailure(*Service, *failures.Failure) error // OnFailure is triggered when a service is failing OnFailure(*Service, *failures.Failure) error // OnFailure is triggered when a service is failing
OnTest() error // OnTest is triggered for testing OnTest() (string, error) // OnTest is triggered for testing
Select() *notifications.Notification // OnTest is triggered for testing Select() *notifications.Notification // OnTest is triggered for testing
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/fatih/structs" "github.com/fatih/structs"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/prometheus/common/log"
Logger "github.com/sirupsen/logrus" Logger "github.com/sirupsen/logrus"
"github.com/statping/statping/types/null" "github.com/statping/statping/types/null"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
@ -16,12 +17,13 @@ import (
) )
var ( var (
Log = Logger.StandardLogger() Log = Logger.StandardLogger()
ljLogger *lumberjack.Logger ljLogger *lumberjack.Logger
LastLines []*logRow LastLines []*logRow
LockLines sync.Mutex LockLines sync.Mutex
VerboseMode int VerboseMode int
version string version string
allowReports bool
) )
const ( const (
@ -29,22 +31,32 @@ const (
errorReporter = "https://ddf2784201134d51a20c3440e222cebe@sentry.statping.com/4" errorReporter = "https://ddf2784201134d51a20c3440e222cebe@sentry.statping.com/4"
) )
func SentryInit(v string) { func SentryInit(v *string, allow bool) {
if v == "" { allowReports = allow
v = "development" if v != nil {
if *v == "" {
*v = "development"
}
version = *v
} }
version = v goEnv := Getenv("GO_ENV", "production").(string)
errorEnv := Getenv("GO_ENV", "production").(string) allowReports := Getenv("ALLOW_REPORTS", false).(bool)
if err := sentry.Init(sentry.ClientOptions{ if allowReports || allow {
Dsn: errorReporter, if err := sentry.Init(sentry.ClientOptions{
Environment: errorEnv, Dsn: errorReporter,
Release: v, Environment: goEnv,
}); err != nil { Release: version,
Log.Errorln(err) }); err != nil {
log.Errorln(err)
}
Log.Infoln("Error Reporting initiated, thank you!")
} }
} }
func SentryErr(err error) { func SentryErr(err error) {
if !allowReports {
return
}
sentry.CaptureException(err) sentry.CaptureException(err)
} }
@ -63,7 +75,7 @@ type hook struct {
func (t *hook) Fire(e *Logger.Entry) error { func (t *hook) Fire(e *Logger.Entry) error {
pushLastLine(e.Message) pushLastLine(e.Message)
if e.Level == Logger.ErrorLevel { if e.Level == Logger.ErrorLevel && allowReports {
SentryLogEntry(e) SentryLogEntry(e)
} }
return nil return nil

View File

@ -1 +1 @@
0.90.23 0.90.25