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:
- name: Checkout code
uses: actions/checkout@master
- name: Create a Sentry.io release
- name: Create a Sentry release
uses: tclindner/sentry-releases-action@v1.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: Statping
SENTRY_PROJECT: golang
SENTRY_ORG: statping
SENTRY_PROJECT: backend
with:
tagName: ${{ github.ref }}
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:
only:
- master
cache:
directories:
- ~/.npm
- ~/.cache
- $GOPATH/pkg/mod
- $GOPATH/src/github.com/statping/statping/frontend/node_modules
env:
global:
- "PATH=$HOME/.local/bin:$PATH"
@ -23,7 +17,6 @@ env:
- DB_USER=travis
- DB_PASS=
- DB_DATABASE=test
- GO_ENV=test
- STATPING_DIR=$GOPATH/src/github.com/statping/statping
go: 1.14
go_import_path: github.com/statping/statping
@ -36,6 +29,7 @@ install:
- "make test-deps yarn clean compile install"
language: go
addons:
chrome: stable
apt:
packages:
- 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
- Added Incident Reporting
- Added Cypress tests
- Added Github and Google OAuth login (beta)
- Added Delete All Failures
- Added Checkin form
- Added Pushover notifier
# 0.90.22
- Added range input types for integer form fields

View File

@ -34,7 +34,7 @@ test: clean
release: test-deps
wget -O statping.gpg $(SIGN_URL)
gpg --import statping.gpg
make build-all
make build-all upload_to_s3
test-ci: clean compile test-deps
SASS=`which sass` go test -v -covermode=count -coverprofile=coverage.out -p=1 ./...
@ -260,11 +260,10 @@ publish-dev:
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
upload_to_s3:
aws s3 cp ./source/css $(ASSETS_BKT) --recursive --exclude "*" --include "*.css"
aws s3 cp ./source/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/scss $(ASSETS_BKT) --recursive --exclude "*" --include "*.scss"
upload_to_s3: travis_s3_creds
aws s3 cp ./source/dist/css $(ASSETS_BKT) --recursive --exclude "*" --include "*.css"
aws s3 cp ./source/dist/js $(ASSETS_BKT) --recursive --exclude "*" --include "*.js"
aws s3 cp ./source/dist/scss $(ASSETS_BKT) --recursive --exclude "*" --include "*.scss"
aws s3 cp ./install.sh $(ASSETS_BKT)
travis_s3_creds:

View File

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

View File

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

View File

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

View File

@ -2,6 +2,12 @@
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', () => {
cy.visit('/setup', {failOnStatusCode: false})
cy.get('#db_connection').select('sqlite')
@ -19,8 +25,34 @@ context('Setup Process', () => {
it('should have sample data', () => {
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('.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.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(2).contains('PRIVATE')
})
@ -48,17 +48,28 @@ context('Groups Tests', () => {
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', () => {
cy.visit('/dashboard/services')
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('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('Test Private Group')
})
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(1).find('.btn-danger').click()
cy.get('.sortable_groups > tr').should('have.length', 3)

View File

@ -2,7 +2,7 @@
import "../support/commands"
context('Messages Tests', () => {
context('Annoucements Tests', () => {
beforeEach(() => {
@ -43,4 +43,10 @@ context('Messages Tests', () => {
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)
})
// uzrwstmtd69hi4wgzsj27q2v29mtpu
it('should confirm notifiers are installed', () => {
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_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', () => {
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)
})
@ -79,7 +79,7 @@ context('Services Tests', () => {
it('should create new ICMP 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_url').clear().type('8.8.8.8')
@ -93,16 +93,20 @@ context('Services Tests', () => {
it('should confirm new 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', () => {
cy.visit('/dashboard/services')
cy.get('#services_list > tr').eq(0).find('.btn-danger').click()
cy.get('#services_list > tr').eq(0).find('.btn-danger').click()
cy.get('#services_list > tr').eq(0).find('.btn-danger').click()
cy.get('#services_list > tr').eq(0).find('.btn-danger').click()
cy.get('#services_list > tr').should('have.length', 4)
cy.get('#services_list > tr').should('have.length', 10)
cy.get('#services_list > tr').eq(0).find('a.btn-danger').click()
cy.get('#services_list > tr').should('have.length', 9)
cy.get('#services_list > tr').eq(1).find('a.btn-danger').click()
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', () => {
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_secret').should('not.have.value', '')
@ -36,7 +36,7 @@ context('Settings Tests', () => {
it('should update Statping 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('#domain').clear().type('http://localhost:8888')
cy.get('#footer').clear().type('Statping Custom Footer')
@ -46,7 +46,7 @@ context('Settings Tests', () => {
it('should confirm Statping 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('#domain').should('have.value', 'http://localhost:8888')
cy.get('#footer').should('have.value', 'Statping Custom Footer')
@ -59,6 +59,13 @@ context('Settings Tests', () => {
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', () => {
cy.visit('/dashboard/settings')
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('button[type="submit"]').click()
cy.get('#users_table > tr').should('have.length', 2)
})
// it('should confirm new user', () => {
// cy.visit('/dashboard/users')
// cy.get('#users_table > tr').should('have.length', 2)
// cy.get('#users_table > tr').eq(0).contains('admin')
// cy.get('#users_table > tr').eq(1).contains('admin2')
// })
it('should create new Admin User', () => {
cy.visit('/dashboard/users')
cy.get('#username').clear().type('admin3')
cy.get('#admin_switch').click()
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) => {
// `on` is used to hook into various events Cypress emits
// `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",
"backend-test": "newman run -e ../dev/postman_environment.json --delay-request 500 ../dev/postman.json",
"cypress:open": "cypress open",
"cypress:test": "cypress run --record --key 49d99e5e-04c6-46df-beef-54b68e152a4d",
"test": "start-server-and-test start http://0.0.0.0:8888/api cypress:test",
"start": "statping -port 8888 > /dev/null 2>&1"
"cypress:test": "cypress run --browser chrome --record false --key $CYPRESS_KEY",
"test": "start-server-and-test start http://localhost:8080/api cypress:test",
"start": "statping -port 8080"
},
"dependencies": {
"@fortawesome/fontawesome-free-solid": "^5.1.0-3",

View File

@ -1,7 +1,11 @@
import Vue from "vue";
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 errorReporter = "https://bed4d75404924cb3a799e370733a1b64@sentry.statping.com/3"
class Api {
constructor() {
@ -9,7 +13,11 @@ class Api {
}
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) {
@ -68,6 +76,10 @@ class Api {
return axios.post('api/reorder/services', data).then(response => (response.data))
}
async checkins() {
return axios.get('api/checkins').then(response => (response.data))
}
async groups() {
return axios.get('api/groups').then(response => (response.data))
}
@ -114,7 +126,7 @@ class Api {
}
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) {
@ -129,6 +141,14 @@ class Api {
return axios.delete('api/incidents/'+incident.id).then(response => (response.data))
}
async checkin_create(data) {
return axios.post('api/checkins', data).then(response => (response.data))
}
async checkin_delete(checkin) {
return axios.delete('api/checkins/'+checkin.api_key).then(response => (response.data))
}
async messages() {
return axios.get('api/messages').then(response => (response.data))
}
@ -246,6 +266,13 @@ class Api {
await axios.all([all])
}
async sentry_init() {
Sentry.init({
dsn: errorReporter,
integrations: [new Integrations.Vue({Vue, attachProps: true})],
});
}
}
const api = new Api()
export default api

View File

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

View File

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

View File

@ -16,7 +16,7 @@
</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 />
</div>
</div>
@ -30,10 +30,21 @@
components: {
ServiceInfo
},
data() {
return {
visible: false
}
},
computed: {
services() {
return this.$store.getters.services
}
},
methods: {
failuresLast24Hours() {
let total = 0;
this.$store.getters.services.map((s) => {
this.services.map((s) => {
total += s.failures_24_hours
})
return total

View File

@ -24,7 +24,7 @@
</thead>
<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">
<font-awesome-icon icon="bars" class="mr-3" /></span> {{group.name}}
</td>
@ -49,7 +49,6 @@
</div>
</div>
<FormGroup v-if="$store.state.admin" :edit="editChange" :in_group="group"/>
</div>
@ -92,9 +91,6 @@
this.$store.commit('setGroups', groups)
}
}
},
beforeMount() {
},
methods: {
editChange(v) {
@ -105,13 +101,6 @@
this.group = g
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) {
let c = confirm(`Are you sure you want to delete '${g.name}'?`)
if (c) {

View File

@ -14,15 +14,22 @@
</thead>
<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 v-if="user.admin"><span class="badge badge-danger">ADMIN</span></td>
<td v-if="!user.admin"><span class="badge badge-primary">USER</span></td>
<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="text-right">
<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="deleteUser(user)" v-if="index !== 0" href="" class="btn btn-danger"><font-awesome-icon icon="times" /></a>
<a @click.prevent="editUser(user, edit)" href="#" class="btn btn-outline-secondary edit-user">
<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>
</td>
</tr>
@ -49,6 +56,11 @@
user: {}
}
},
computed: {
users() {
return this.$store.getters.users
}
},
methods: {
editChange(v) {
this.user = {}

View File

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

View File

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

View File

@ -26,10 +26,6 @@
<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>
</li>
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/help" class="nav-link">Help</router-link>
</li>
</ul>
<span class="navbar-text">
<a href="#" class="nav-link" v-on:click="logout">Logout</a>
@ -55,7 +51,7 @@
this.$store.commit('setHasAllData', false)
this.$store.commit('setToken', null)
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>
<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">
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> |
<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 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>
</template>
@ -18,15 +18,11 @@
components: {
Dashboard
},
props: {
version: String,
logged_in: Boolean
},
watch: {
logged_in() {
}
computed: {
core() {
return this.$store.getters.core
}
}
}
</script>

View File

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

View File

@ -34,7 +34,7 @@ export default {
},
methods: {
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")
}
}

View File

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

View File

@ -35,10 +35,12 @@
}
},
async mounted() {
this.value = this.func.value;
this.title = this.func.title;
this.subtitle = this.func.subtitle;
this.chart = this.convertToChartData(this.func.chart);
if (this.func) {
this.value = this.func.value;
this.title = this.func.title;
this.subtitle = this.func.subtitle;
this.chart = this.convertToChartData(this.func.chart);
}
},
async latencyYesterday() {
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>
</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">
<li class="page-item" :class="{'disabled': page===1}">
<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>
</a>
</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>
</li>
<li class="page-item" :class="{'disabled': page===Math.floor(total / limit)}">
@ -53,6 +53,19 @@ export default {
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 () {
await this.gotoPage(1)
},

View File

@ -1,23 +1,28 @@
<template v-if="service">
<div class="col-12 card mb-4" style="min-height: 280px;" :class="{'offline-card': !service.online}">
<div class="card-body p-3 p-md-1 pt-md-3 pb-md-1">
<h4 class="card-title mb-4">
<template>
<div class="card mb-4" :class="{'offline-card': !service.online}">
<div class="card-title px-4 pt-3">
<h4 v-observe-visibility="setVisible">
<router-link :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge float-right" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
{{service.online ? "ONLINE" : "OFFLINE"}}
</span>
{{service.online ? "ONLINE" : "OFFLINE"}}
</span>
</h4>
</div>
<div class="card-body p-3 p-md-1 pt-md-3 pb-md-1">
<transition name="fade">
<div v-if="loaded && service.online" class="row pb-3">
<div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-3">
<ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/>
</div>
<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 v-if="loaded && service.online" class="col-12 pb-2">
<div class="row">
<div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-3">
<ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/>
</div>
<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 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"
title="Since Yesterday"
@ -44,21 +49,35 @@
group="24h" expression="latencyPercent"/>
</div>
<div class="col-4">
<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-4">
</div>
</transition>
</div>
<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>
</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'}">
Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span></button>
</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">
<FormIncident :service="service" />
</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">
<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>
</transition>
</div>
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
@ -79,6 +96,7 @@
</template>
<script>
import Checkin from '../../forms/Checkin';
import FormIncident from '../../forms/Incident';
import FormMessage from '../../forms/Message';
import ServiceFailures from './ServiceFailures';
@ -89,6 +107,7 @@
export default {
name: 'ServiceInfo',
components: {
Checkin,
ServiceFailures,
FormIncident,
FormMessage,
@ -109,17 +128,27 @@
loaded: false,
set1_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_name = this.calc(this.set1)
this.set2 = await this.getHits(24, "1h")
this.set2_name = this.calc(this.set2)
this.loaded = true
},
methods: {
},
async deleteFailures() {
const c = confirm('Are you sure you want to delete all failures?')
if (c) {

View File

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

View File

@ -31,7 +31,20 @@
<small class="form-text text-muted">HTML is allowed inside the footer</small>
</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>
</template>
@ -41,24 +54,17 @@
export default {
name: 'CoreSettings',
props: {
in_core: {
type: Object,
required: true,
computed: {
core() {
return this.$store.getters.core
}
},
data() {
return {
core: this.in_core
}
},
},
methods: {
async saveSettings() {
const c = this.core
await Api.core_save(c)
const core = await Api.core()
this.$store.commit('setCore', core)
this.core = core
},
selectAll() {
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 data = {name: g.name, public: g.public}
await Api.group_create(data)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
await this.update()
this.group = {}
},
async updateGroup() {
const g = this.group
const data = {id: g.id, name: g.name, public: g.public}
await Api.group_update(data)
await this.update()
this.edit(false)
},
async update() {
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
this.edit(false)
}
}
}

View File

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

View File

@ -23,10 +23,6 @@
</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>
</template>
@ -35,11 +31,14 @@
export default {
name: 'FormLogin',
props: {
oauth: {
type: Object
}
},
computed: {
core() {
return this.$store.getters.core
},
oauth() {
return this.$store.getters.core.oauth
}
},
data() {
return {
username: "",

View File

@ -35,20 +35,25 @@
</div>
</div>
<div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div>
<div v-if="success" class="alert alert-success col-12" role="alert">{{notifier.title}} appears to be working!</div>
<div v-if="error && !success" class="alert alert-danger col-12" role="alert">
{{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-body">
<div class="row">
<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"}}
</button>
</div>
<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>
</div>
</div>
@ -79,6 +84,7 @@ export default {
loading: false,
loadingTest: false,
error: null,
response: null,
success: false,
saved: false,
form: {},
@ -130,6 +136,7 @@ export default {
} else {
this.error = tested.error
}
this.response = tested.response
this.loadingTest = false
},
}

View File

@ -6,8 +6,8 @@
<div class="form-group row">
<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">
<span @click="internal_enabled = !!internal_enabled" class="switch float-left">
<input v-model="internal_enabled" type="checkbox" class="switch" id="switch-local-oauth" :checked="internal_enabled">
<span @click="oauth.internal_enabled = !!core.oauth.internal_enabled" class="switch float-left">
<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>
</span>
</div>
@ -15,7 +15,7 @@
<div class="form-group row">
<label for="whitelist_domains" class="col-sm-4 col-form-label">Whitelist Domains</label>
<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>
@ -28,20 +28,20 @@
<div class="form-group row mt-3">
<label for="github_client" class="col-sm-4 col-form-label">Github Client ID</label>
<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 class="form-group row">
<label for="github_secret" class="col-sm-4 col-form-label">Github Client Secret</label>
<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 class="form-group row">
<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">
<span @click="github_enabled = !!github_enabled" class="switch float-left">
<input v-model="github_enabled" type="checkbox" class="switch" id="switch-gh-oauth" :checked="github_enabled">
<span @click="oauth.github_enabled = !!oauth.github_enabled" class="switch float-left">
<input v-model="oauth.github_enabled" type="checkbox" class="switch" id="switch-gh-oauth" :checked="oauth.github_enabled">
<label for="switch-gh-oauth"> </label>
</span>
</div>
@ -49,7 +49,12 @@
<div class="form-group row">
<label for="gh_callback" class="col-sm-4 col-form-label">Callback URL</label>
<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>
@ -81,9 +86,14 @@
</div>
</div>
<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">
<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>
@ -124,14 +134,17 @@
<div class="form-group row">
<label for="slack_callback" class="col-sm-4 col-form-label">Callback URL</label>
<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>
{{providers()}}
<button class="btn btn-primary btn-block" @click.prevent="saveOAuth" type="submit">
Save OAuth Settings
</button>
@ -144,25 +157,34 @@
export default {
name: 'OAuth',
props: {
oauth: {
type: Object
}
},
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')
computed: {
oauth() {
return this.$store.getters.core.oauth
}
},
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() {
// this.github_enabled = this.$store.getters.core.oauth.oauth_providers.split(",").includes('github')
// const c = await Api.core()
// this.auth = c.auth
},
methods: {
has(val) {
if (!this.core.oauth.oauth_providers) {
return false
}
return this.core.oauth.oauth_providers.split(",").includes(val)
},
providers() {
let providers = [];
if (this.github_enabled) {
@ -186,7 +208,6 @@
await Api.core_save(c)
const core = await Api.core()
this.$store.commit('setCore', core)
this.core = core
}
}
}

View File

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

View File

@ -1,9 +1,10 @@
import {library} from '@fortawesome/fontawesome-svg-core'
import {fas} from '@fortawesome/fontawesome-free-solid';
import {fab} from '@fortawesome/free-brands-svg-icons';
import {far} from '@fortawesome/fontawesome-svg-core';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import Vue from "vue";
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 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 "./mixin"
import "./icons"
@ -19,12 +17,6 @@ Vue.use(VueClipboard);
Vue.use(VueRouter);
Vue.use(VueObserveVisibility);
Sentry.init({
dsn: errorReporter,
integrations: [new Integrations.Vue({Vue, attachProps: true})],
});
Vue.config.productionTip = false
new Vue({
router,

View File

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

View File

@ -3,7 +3,7 @@
<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">
<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>
@ -16,16 +16,16 @@
</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 />
</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"/>
</div>
<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 />
</div>
</div>
@ -58,6 +58,20 @@ export default {
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() {
this.logged_in = this.loggedIn()
},
@ -69,11 +83,6 @@ export default {
const start = this.isBetween(new Date(), message.start_on)
const end = this.isBetween(message.end_on, new Date())
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="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">
<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>
<FormLogin :oauth="$store.getters.core.oauth"/>
<FormLogin/>
</div>
</div>
</template>

View File

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

View File

@ -21,8 +21,8 @@
<h6 class="mt-4 text-muted">Notifiers</h6>
<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">
<font-awesome-icon :icon="iconName(notifier.icon)" class="mr-2"/> {{notifier.method}}
<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.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>
</a>
</div>
@ -60,11 +60,10 @@
<div class="card text-black-50 bg-white">
<div class="card-header">Statping Settings</div>
<div class="card-body">
<CoreSettings :in_core="core"/>
<CoreSettings/>
</div>
</div>
<div class="card text-black-50 bg-white mt-3">
<div class="card-header">API Settings</div>
<div class="card-body">
@ -73,7 +72,7 @@
<div class="col-sm-9">
<div class="input-group">
<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>
</div>
</div>
@ -86,12 +85,12 @@
<div class="col-sm-9">
<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>
<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>
</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">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>
@ -112,7 +111,7 @@
<div class="card text-black-50 bg-white mb-5">
<div class="card-header">Theme Editor</div>
<div class="card-body">
<ThemeEditor :core="core"/>
<ThemeEditor/>
</div>
</div>
</div>
@ -127,10 +126,10 @@
</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">
<OAuth :oauth="core.oauth"/>
<OAuth/>
</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"/>
</div>
@ -167,20 +166,24 @@
tab: "v-pills-home-tab",
qrcode: "",
qrurl: "",
core: this.$store.getters.core
}
},
computed: {
core() {
return this.$store.getters.core
},
notifiers() {
return this.$store.getters.notifiers
}
},
async mounted() {
this.cache = await Api.cache()
},
async created() {
const c = this.$store.state.core
const c = this.core
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)
},
async beforeMount() {
this.core = await Api.core()
},
methods: {
changeTab(e) {
this.tab = e.target.id

View File

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

View File

@ -81,6 +81,8 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) {
}
app.OAuth = c.OAuth
app.UseCdn = null.NewNullBool(c.UseCdn.Bool)
app.AllowReports = null.NewNullBool(c.AllowReports.Bool)
utils.SentryInit(nil, app.AllowReports.Bool)
err = app.Update()
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
// 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
func (s Storage) Get(key string) []byte {
s.mu.Lock()
defer s.mu.Unlock()
item := s.items[key]
if item.Expired() {
CacheStorage.Delete(key)

View File

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

View File

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

View File

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

View File

@ -8,11 +8,6 @@ import (
"net/http"
)
func apiAllIncidentsHandler(w http.ResponseWriter, r *http.Request) {
inc := incidents.All()
returnJson(inc, w, r)
}
func apiServiceIncidentsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
incids := incidents.FindByService(utils.ToInt(vars["id"]))

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ func (c *commandLine) Select() *notifications.Notification {
var Command = &commandLine{&notifications.Notification{
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.",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
@ -33,37 +33,30 @@ var Command = &commandLine{&notifications.Notification{
Form: []notifications.NotificationForm{{
Type: "text",
Title: "Shell or Bash",
Placeholder: "/bin/bash",
Placeholder: "/usr/bin/curl",
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",
Title: "Command to Run on OnSuccess",
Placeholder: "curl google.com",
Placeholder: "http://localhost:8080/health",
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",
Title: "Command to Run on OnFailure",
Placeholder: "curl offline.com",
Placeholder: "http://localhost:8080/health",
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) {
utils.Log.Infof("Command notifier sending: %s %s", app, strings.Join(cmd, " "))
outStr, errStr, err := utils.Command(app, cmd...)
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
func (c *commandLine) OnSuccess(s *services.Service) error {
msg := c.GetValue("var1")
@ -72,11 +65,19 @@ func (c *commandLine) OnSuccess(s *services.Service) error {
return err
}
// OnTest for commandLine triggers when this notifier has been saved
func (c *commandLine) OnTest() error {
cmds := strings.Split(c.Var1, " ")
in, out, err := runCommand(c.Host, cmds...)
utils.Log.Infoln(in)
utils.Log.Infoln(out)
// 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
}
// 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"
"encoding/json"
"errors"
"fmt"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/notifications"
"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
func (d *discord) OnTest() error {
func (d *discord) OnTest() (string, error) {
outError := errors.New("Incorrect discord URL, please confirm URL is correct")
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)
if string(contents) == "" {
return nil
return "", nil
}
var dtt discordTestJson
err = json.Unmarshal(contents, &dtt)
if err != nil {
return outError
return string(contents), outError
}
if dtt.Code == 0 {
return outError
return string(contents), outError
}
fmt.Println("discord: ", string(contents))
return nil
return string(contents), nil
}
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
func (e *emailer) OnTest() error {
func (e *emailer) OnTest() (string, error) {
testService := &services.Service{
Id: 1,
Name: "Example Service",
@ -201,14 +201,16 @@ func (e *emailer) OnTest() error {
LastResponse: "<html>this is an example response</html>",
CreatedAt: utils.Now().Add(-24 * time.Hour),
}
subject := fmt.Sprintf("Service %v is Back Online", testService.Name)
email := &emailOutgoing{
To: e.Var2,
Subject: fmt.Sprintf("Service %v is Back Online", testService.Name),
Subject: subject,
Template: mainEmailTemplate,
Data: testService,
From: e.Var1,
}
return e.dialSend(email)
err := e.dialSend(email)
return subject, err
}
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
func (l *lineNotifier) sendMessage(message string) error {
func (l *lineNotifier) sendMessage(message string) (string, error) {
v := url.Values{}
v.Set("message", message)
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)
return err
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 string(content), err
}
// OnFailure will trigger failing service
func (l *lineNotifier) OnFailure(s *services.Service, f *failures.Failure) error {
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
func (l *lineNotifier) OnSuccess(s *services.Service) error {
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
func (l *lineNotifier) OnTest() error {
func (l *lineNotifier) OnTest() (string, error) {
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{
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.
<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",
@ -97,7 +97,7 @@ func (m *mobilePush) OnSuccess(s *services.Service) error {
}
// OnTest triggers when this notifier has been saved
func (m *mobilePush) OnTest() error {
func (m *mobilePush) OnTest() (string, error) {
msg := &pushArray{
Message: "Testing the Mobile Notifier",
Title: "Testing Notifications",
@ -107,18 +107,18 @@ func (m *mobilePush) OnTest() error {
}
body, err := pushRequest(msg)
if err != nil {
return err
return "", err
}
var output mobileResponse
err = json.Unmarshal(body, &output)
if err != nil {
return err
return string(body), err
}
if len(output.Logs) == 0 {
return nil
return string(body), err
} else {
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,
Webhook,
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
}
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)
if err != nil {
return err
return "", err
}
defer resp.Body.Close()
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

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
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)
v := url.Values{}
@ -65,27 +65,30 @@ func (t *telegram) sendMessage(message string) error {
if !success {
errorOut := telegramError(contents)
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
func (t *telegram) OnFailure(s *services.Service, f *failures.Failure) error {
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
func (t *telegram) OnSuccess(s *services.Service) error {
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
func (t *telegram) OnTest() error {
func (t *telegram) OnTest() (string, error) {
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) {

View File

@ -25,6 +25,7 @@ func init() {
}
func TestTelegramNotifier(t *testing.T) {
t.SkipNow()
db, err := database.OpenTester()
require.Nil(t, err)
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
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"))
v := url.Values{}
@ -75,25 +75,27 @@ func (t *twilio) sendMessage(message string) error {
if !success {
errorOut := twilioError(contents)
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
func (t *twilio) OnFailure(s *services.Service, f *failures.Failure) error {
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
func (t *twilio) OnSuccess(s *services.Service) error {
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
func (t *twilio) OnTest() error {
func (t *twilio) OnTest() (string, error) {
msg := fmt.Sprintf("Testing the Twilio SMS Notifier")
return t.sendMessage(msg)
}

View File

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

View File

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

View File

@ -57,7 +57,7 @@ func Connect(configs *DbConfig, retry bool) error {
if err != nil {
log.Debugln(fmt.Sprintf("Database connection error %s", err))
if retry {
log.Errorln(fmt.Sprintf("Database %s connection to '%s' is not available, trying again in 5 seconds...", configs.DbConn, configs.DbHost))
log.Warnln(fmt.Sprintf("Database %s connection to '%s' is not available, trying again in 5 seconds...", configs.DbConn, configs.DbHost))
time.Sleep(5 * time.Second)
return Connect(configs, retry)
} else {

View File

@ -30,7 +30,13 @@ func Select() (*Core, error) {
return nil, db.Error()
}
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()
}

View File

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

View File

@ -9,7 +9,7 @@ func Samples() error {
incident1 := &Incident{
Title: "Github Issues",
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 {
return err
@ -18,7 +18,7 @@ func Samples() error {
incident2 := &Incident{
Title: "Recent Downtime",
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 {
return err

View File

@ -9,5 +9,5 @@ import (
type Notifier interface {
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
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 {
OnSuccess(*Service) error // OnSuccess is triggered when a service is successful
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
}

View File

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

View File

@ -1 +1 @@
0.90.23
0.90.25