Merge pull request #663 from statping/local-assets-fix

Local assets fix
pull/679/head
Hunter Long 2020-06-15 01:52:10 -07:00 committed by GitHub
commit fa07d46676
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 802 additions and 368 deletions

View File

@ -1,3 +1,10 @@
# 0.90.51 (06-15-2020)
- Fix Theme Editor codemirror inputs to show on load
- Added favicon folder for local assets can be used without remote access
- Modified Notifier's to return the response as a string for the frontend
- Modified Notifiers so they can use custom data for their request
- Added Notifier OnSuccess and onFailure custom data on frontend
# 0.90.50 (06-13-2020)
- Removed PORT, replaced with SERVER_PORT
- Removed HOST/IP, replaced with SERVER_IP

View File

@ -125,6 +125,7 @@ frontend-build:
cd frontend && yarn && yarn build
cp -r frontend/dist source/ && cp -r frontend/src/assets/scss source/dist/
cp -r source/tmpl/*.* source/dist/
cp -r frontend/public/favicon source/dist/
frontend-copy:
cp -r source/tmpl/*.* source/dist/

2
dev/postman.json vendored
View File

@ -4233,7 +4233,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"method\": \"slack\",\n \"host\": \"https://hooks.slack.com/services/EXAMPLEIDHERE/BV33WKP0C/MtKw3Kc8BFylTv4pohKqHtXX\",\n \"enabled\": true,\n \"limits\": 55\n}",
"raw": "{\n \"enabled\": false,\n \"limits\": 60,\n \"method\": \"slack\",\n \"host\": \"https://webhooksurl.slack.com/***\",\n \"success_data\": \"{\\n \\\"blocks\\\": [{\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"The service {{.Service.Name}} is back online.\\\"\\n }\\n }, {\\n \\\"type\\\": \\\"actions\\\",\\n \\\"elements\\\": [{\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"View Service\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"style\\\": \\\"primary\\\",\\n \\\"url\\\": \\\"{{.Core.Domain}}/service/{{.Service.Id}}\\\"\\n }, {\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"Go to Statping\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"url\\\": \\\"{{.Core.Domain}}\\\"\\n }]\\n }]\\n}\",\n \"failure_data\": \"{\\n \\\"blocks\\\": [{\\n \\\"type\\\": \\\"section\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\":warning: The service {{.Service.Name}} is currently offline! :warning:\\\"\\n }\\n }, {\\n \\\"type\\\": \\\"divider\\\"\\n }, {\\n \\\"type\\\": \\\"section\\\",\\n \\\"fields\\\": [{\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Service:*\\\\n{{.Service.Name}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*URL:*\\\\n{{.Service.Domain}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Status Code:*\\\\n{{.Service.LastStatusCode}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*When:*\\\\n{{.Failure.CreatedAt}}\\\"\\n }, {\\n \\\"type\\\": \\\"mrkdwn\\\",\\n \\\"text\\\": \\\"*Downtime:*\\\\n{{.Service.DowntimeAgo}}\\\"\\n }, {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"*Error:*\\\\n{{.Failure.Issue}}\\\"\\n }]\\n }, {\\n \\\"type\\\": \\\"divider\\\"\\n }, {\\n \\\"type\\\": \\\"actions\\\",\\n \\\"elements\\\": [{\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"View Offline Service\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"style\\\": \\\"danger\\\",\\n \\\"url\\\": \\\"{{.Core.Domain}}/service/{{.Service.Id}}\\\"\\n }, {\\n \\\"type\\\": \\\"button\\\",\\n \\\"text\\\": {\\n \\\"type\\\": \\\"plain_text\\\",\\n \\\"text\\\": \\\"Go to Statping\\\",\\n \\\"emoji\\\": true\\n },\\n \\\"url\\\": \\\"{{.Core.Domain}}\\\"\\n }]\\n }]\\n}\"\n}",
"options": {
"raw": {}
}

View File

@ -28,6 +28,7 @@
"codemirror-colorpicker": "^1.9.66",
"core-js": "^3.4.4",
"date-fns": "^2.9.0",
"js-beautify": "^1.11.0",
"querystring": "^0.2.0",
"vue": "^2.6.11",
"vue-apexcharts": "^1.5.2",

View File

@ -1,48 +1,47 @@
{{ define "base" }}
<!DOCTYPE html>
{{ define "base" }}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{CoreApp.Name}} - Service Monitoring</title>
<meta name="title" content="{{CoreApp.Name}} - Service Monitoring">
<meta name="description" content="{{CoreApp.Description}} {{CoreApp.Name}}">
<base href="{{BasePath}}">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, shrink-to-fit=no">
<meta name="description" content="{{CoreApp.Description}}">
<link rel="apple-touch-icon" sizes="57x57" href="https://assets.statping.com/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="https://assets.statping.com/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="https://assets.statping.com/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="https://assets.statping.com/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="https://assets.statping.com/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="https://assets.statping.com/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="https://assets.statping.com/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="https://assets.statping.com/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://assets.statping.com/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="https://assets.statping.com/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://assets.statping.com/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="https://assets.statping.com/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://assets.statping.com/favicon/favicon-16x16.png">
<link rel="manifest" href="https://assets.statping.com/favicon/manifest.json">
<link rel="apple-touch-icon" sizes="57x57" href="favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png">
<link rel="manifest" href="favicon/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="https://assets.statping.com/favicon/ms-icon-144x144.png">
<meta name="msapplication-TileImage" content="favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<link rel="shortcut icon" href="https://assets.statping.com/favicon/favicon.ico" type="image/x-icon">
<link rel="icon" href="https://assets.statping.com/favicon/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="favicon/favicon.ico" type="image/x-icon">
<link rel="icon" href="favicon/favicon.ico" type="image/x-icon">
<meta property="og:type" content="website">
<meta property="og:url" content="{{CoreApp.Domain}}">
<meta property="og:title" content="{{CoreApp.Name}} Service Monitoring">
<meta property="og:description" content="{{CoreApp.Description}} {{CoreApp.Name}}">
<meta property="og:image" content="https://assets.statping.com/statping_social.png">
<meta property="og:image" content="favicon/social.png">
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="{{CoreApp.Domain}}">
<meta property="twitter:title" content="{{CoreApp.Name}} Service Monitoring">
<meta property="twitter:description" content="{{CoreApp.Description}} {{CoreApp.Name}}">
<meta property="twitter:image" content="https://assets.statping.com/statping_social.png">
<meta property="twitter:image" content="favicon/social.png">
<base href="{{BasePath}}">
{{if USE_CDN}}
<link rel="stylesheet" href="https://assets.statping.com/vendor.css">
<link rel="stylesheet" href="https://assets.statping.com/style.css">

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,41 @@
{
"name": "Statping",
"icons": [
{
"src": "favicon\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "favicon\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "favicon\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "favicon\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "favicon\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "favicon\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -190,8 +190,8 @@ class Api {
return axios.post('api/notifier/' + data.method, data).then(response => (response.data))
}
async notifier_test(data) {
return axios.post('api/notifier/' + data.method + '/test', data).then(response => (response.data))
async notifier_test(data, notifier) {
return axios.post('api/notifier/' + notifier + '/test', data).then(response => (response.data))
}
async renewApiKeys() {

View File

@ -395,6 +395,30 @@ HTML,BODY {
color: #a0a0a0;
}
.service_block {
min-height: 340px;
}
.json-field {
font-size: 10pt;
}
.cm-bracketer {
color: #a011c9;
font-weight: bold;
}
.cm-var-highlight {
color: #0fa50f;
font-weight: bold;
}
.cm-var-sub-highlight {
color: #b40727;
font-weight: bold;
}
.card {
background-color: $service-background;
border: $service-border;

View File

@ -0,0 +1,20 @@
import CodeMirror from 'codemirror'
CodeMirror.defineMode('mymode', () => {
return {
token(stream, state) {
if (stream.match(".Service") || (stream.match(".Core")) || (stream.match(".Failure"))) {
return "var-highlight"
} else if (stream.match(".Id") || stream.match(".Domain") || stream.match(".CreatedAt") ||
stream.match(".Name") || stream.match(".DowntimeAgo") || stream.match(".Issue") || stream.match(".LastStatusCode") ||
stream.match(".Port") || stream.match(".FailuresLast24Hours") || stream.match(".PingTime")) {
return "var-sub-highlight"
} else if (stream.match("{{") || stream.match("}}")) {
return "bracketer"
} else {
stream.next()
return null
}
}
}
})

View File

@ -4,15 +4,15 @@
<div class="row stats_area mb-5">
<div class="col-4">
<span class="font-6 font-weight-bold d-block">{{$store.getters.services.length}}</span>
<span class="font-2">Total Services</span>
<span class="font-2">{{ $t('dashboard.total_services') }}</span>
</div>
<div class="col-4">
<span class="font-6 font-weight-bold d-block">{{failuresLast24Hours()}}</span>
<span class="font-2">Failures last 24 Hours</span>
<span class="font-2">{{ $t('dashboard.failures_24_hours') }}</span>
</div>
<div class="col-4">
<span class="font-6 font-weight-bold d-block">{{$store.getters.onlineServices(true).length}}</span>
<span class="font-2">Online Services</span>
<span class="font-2">{{ $t('dashboard.online_services') }}</span>
</div>
</div>

View File

@ -31,8 +31,8 @@
</td>
<td>{{$store.getters.servicesInGroup(group.id).length}}</td>
<td>
<span v-if="group.public" class="badge badge-primary">PUBLIC</span>
<span v-if="!group.public" class="badge badge-secondary">PRIVATE</span>
<span v-if="group.public" class="badge badge-primary text-uppercase">{{ $t('public') }}</span>
<span v-if="!group.public" class="badge badge-secondary text-uppercase">{{ $t('private') }}</span>
</td>
<td class="text-right">
<div v-if="$store.state.admin" class="btn-group">

View File

@ -6,9 +6,9 @@
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Username</th>
<th scope="col">Type</th>
<th scope="col" class="d-none d-md-table-cell">Last Login</th>
<th scope="col">{{$t('username')}}</th>
<th scope="col">{{$t('type')}}</th>
<th scope="col" class="d-none d-md-table-cell">{{ $t('last_login') }}</th>
<th scope="col"></th>
</tr>
</thead>
@ -17,15 +17,15 @@
<tr v-for="(user, index) in users" v-bind:key="user.id" >
<td>{{user.username}}</td>
<td>
<span class="badge" :class="{'badge-danger': user.admin, 'badge-primary': !user.admin}">
{{user.admin ? 'ADMIN' : 'USER'}}
<span class="badge text-uppercase" :class="{'badge-danger': user.admin, 'badge-primary': !user.admin}">
{{user.admin ? $t('admin') : $t('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 edit-user">
<font-awesome-icon icon="user" /> Edit
<font-awesome-icon icon="user" /> {{$t('edit') }}
</a>
<a @click.prevent="deleteUser(user)" v-if="index !== 0" href="#" class="btn btn-danger delete-user">
<font-awesome-icon icon="times" />

View File

@ -4,7 +4,7 @@
<tr>
<th scope="col">Name</th>
<th scope="col" class="d-none d-md-table-cell">Visibility</th>
<th scope="col" class="d-none d-md-table-cell">Group</th>
<th scope="col" class="d-none d-md-table-cell">{{ $t('group') }}</th>
<th scope="col"></th>
</tr>
</thead>
@ -17,7 +17,7 @@
</td>
<td class="d-none d-md-table-cell">
<span class="badge" :class="{'badge-primary': service.public, 'badge-secondary': !service.public}">
{{service.public ? "PUBLIC" : "PRIVATE"}}
{{service.public ? $t('public') : $t('private')}}
</span>
</td>
<td class="d-none d-md-table-cell">

View File

@ -1,4 +1,4 @@
<template >
<template>
<div>
<div v-if="loaded && !directory" class="jumbotron jumbotron-fluid">
<div class="text-center col-12">
@ -8,7 +8,7 @@
</p></span>
</div>
</div>
<form v-if="loaded && directory" @submit.prevent="saveAssets" :disabled="pending">
<form v-observe-visibility="visible" v-if="loaded && directory" @submit.prevent="saveAssets" :disabled="pending">
<h3>Variables</h3>
<codemirror v-show="loaded" v-model="vars" ref="vars" :options="cmOptions" class="codemirrorInput"/>
@ -77,6 +77,13 @@
this.changeTab('vars')
},
methods: {
visible(isVisible, entry) {
if (isVisible) {
this.$refs.vars.codemirror.refresh()
this.$refs.base.codemirror.refresh()
this.$refs.mobile.codemirror.refresh()
}
},
async fetchTheme() {
this.loaded = true
this.pending = true
@ -121,20 +128,18 @@
},
changeTab (v) {
this.tab = v
if (v === 'base') {
this.$refs.base.codemirror.refresh();
} else if (v === 'vars') {
this.$refs.vars.codemirror.refresh();
} else if (v === 'mobile') {
this.$refs.mobile.codemirror.refresh();
}
// if (v === 'base') {
// this.$refs.base.codemirror.refresh();
// } else if (v === 'vars') {
// this.$refs.vars.codemirror.refresh();
// } else if (v === 'mobile') {
// this.$refs.mobile.codemirror.refresh();
// }
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
<style scoped>
.CodeMirror {
border: 1px solid #eee;
height: 550px;

View File

@ -9,26 +9,26 @@
<div class="navbar-collapse" :class="{collapse: !navopen}" id="navbarText">
<ul class="navbar-nav mr-auto">
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard" class="nav-link">Dashboard</router-link>
<router-link to="/dashboard" class="nav-link">{{ $t('top_nav.dashboard') }}</router-link>
</li>
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/services" class="nav-link">Services</router-link>
<router-link to="/dashboard/services" class="nav-link">{{ $t('top_nav.services') }}</router-link>
</li>
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/users" class="nav-link">Users</router-link>
<router-link to="/dashboard/users" class="nav-link">{{ $t('top_nav.users') }}</router-link>
</li>
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/messages" class="nav-link">Announcements</router-link>
<router-link to="/dashboard/messages" class="nav-link">{{ $t('top_nav.announcements') }}</router-link>
</li>
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/settings" class="nav-link">Settings</router-link>
<router-link to="/dashboard/settings" class="nav-link">{{ $t('top_nav.settings') }}</router-link>
</li>
<li v-if="$store.state.admin" @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/logs" class="nav-link">Logs</router-link>
<router-link to="/dashboard/logs" class="nav-link">{{ $t('top_nav.logs') }}</router-link>
</li>
</ul>
<span class="navbar-text">
<a href="#" class="nav-link" @click.prevent="logout">Logout</a>
<a href="#" class="nav-link" @click.prevent="logout">{{ $t('top_nav.logout') }}</a>
</span>
</div>
</nav>

View File

@ -47,7 +47,9 @@
{{intervalpick.text}}
</button>
<div class="service-tm-menu" :class="{'d-none': !intervalMenu}">
<a v-for="(interval, i) in intervals" @click.prevent="changeInterval(interval)" class="dropdown-item" href="#" :class="{'active': intervalpick === interval, 'disabled': disabled_interval(interval)}">{{interval.text}}</a>
<a v-for="(interval, i) in intervals" @click.prevent="changeInterval(interval)" class="dropdown-item" href="#" :class="{'active': intervalpick === interval, 'disabled': disabled_interval(interval)}">
{{interval.text}}
</a>
</div>
<span class="d-none float-left d-md-inline">

View File

@ -1,18 +1,18 @@
<template>
<form @submit.prevent="saveSettings">
<div class="form-group">
<label>Project Name</label>
<label>{{ $t('settings.name') }}</label>
<input v-model="core.name" type="text" class="form-control" placeholder="Great Uptime" id="project">
</div>
<div class="form-group">
<label>Project Description</label>
<label>{{ $t('settings.description') }}</label>
<input v-model="core.description" type="text" class="form-control" placeholder="Great Uptime" id="description">
</div>
<div class="form-group row">
<div class="col-8 col-sm-9">
<label>Domain</label>
<label>{{ $t('domain') }}</label>
<input v-model="core.domain" type="url" class="form-control" id="domain">
</div>
<div class="col-4 col-sm-3 mt-sm-1 mt-0">
@ -26,25 +26,24 @@
</div>
<div class="form-group">
<label>Custom Footer</label>
<label>{{ $t('settings.footer') }}</label>
<textarea v-model="core.footer" rows="4" class="form-control" id="footer">{{core.footer}}</textarea>
<small class="form-text text-muted">HTML is allowed inside the footer</small>
<small class="form-text text-muted">{{ $t('settings.footer_notes') }}</small>
</div>
<div class="form-group">
<label>Language</label>
<select v-model="core.language" class="form-control">
<label>{{ $t('setup.language') }}</label>
<select v-model="core.language" v-bind:value="core.language" class="form-control">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="ru">Russian</option>
<option value="de">German</option>
</select>
<small class="form-text text-muted">HTML is allowed inside the footer</small>
</div>
<div class="form-group row mt-3">
<label class="col-sm-10 col-form-label">Enable Error Reporting</label>
<label class="col-sm-10 col-form-label">{{ $t('settings.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">
@ -52,12 +51,12 @@
</span>
</div>
<div class="col-12">
<small>Help the Statping project out by sending anonymous error logs back to our server.</small>
<small>{{ $t('settings.error_reporting_notes') }}</small>
</div>
</div>
<button @click.prevent="saveSettings" id="save_core" type="submit" class="btn btn-primary btn-block mt-3" v-bind:disabled="loading">
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>Save Settings
<font-awesome-icon v-if="loading" icon="circle-notch" class="mr-2" spin/>{{ $t('settings.save') }}
</button>
</form>

View File

@ -1,5 +1,6 @@
<template>
<div>
<form @submit.prevent="saveNotifier">
<div class="card contain-card text-black-50 bg-white mb-3">
<div class="card-header text-capitalize">
{{notifier.title}}
@ -9,9 +10,6 @@
</span>
</div>
<div class="card-body">
<form @submit.prevent="saveNotifier">
<p class="small text-muted" v-html="notifier.description"/>
<div v-for="(form, index) in notifier.form" v-bind:key="index" class="form-group">
@ -30,31 +28,73 @@
</div>
</div>
</form>
</div>
</div>
<div v-if="error && !success" class="alert alert-danger col-12" role="alert">
{{error}}<p v-if="response">Response:<br>{{response}}</p>
<div v-if="notifier.data_type" class="card text-black-50 bg-white mb-3">
<div class="card-header text-capitalize">
{{notifier.title}} Outgoing Request
<span class="badge badge-dark float-right text-uppercase mt-1">{{notifier.data_type}}</span>
</div>
<div class="card-body">
<span class="text-muted d-block mb-3" v-if="notifier.request_info" v-html="notifier.request_info"></span>
<div class="row" v-observe-visibility="visible">
<div class="col-12">
<h5 class="text-capitalize">Success Data</h5>
<codemirror v-model="success_data"
ref="cmsuccess"
:options="cmOptions"
@ready="onCmSuccessReady"/>
</div>
</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 class="row mt-4">
<div class="col-12">
<h5 class="text-capitalize">Failure Data</h5>
<codemirror v-model="failure_data"
ref="cmfailure"
:options="cmOptions"
@ready="onCmFailureReady"/>
</div>
</div>
</div>
</div>
</form>
<div v-if="error || success" class="card text-black-50 bg-white mb-3">
<div class="card-body">
<div v-if="error && !success" class="alert alert-danger col-12" role="alert">
{{error}}
</div>
<div v-if="success" class="alert alert-success col-12" role="alert">
<span class="text-capitalize">{{notifier.title}}</span> appears to be working!
</div>
<h5>Response</h5>
<codemirror :value="response"/>
</div>
</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">
<div class="col-4 col-sm-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
<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 test-notifier"><i class="fa fa-vial"></i>
{{loadingTest ? "Loading..." : "Test Notifier"}}</button>
<div class="col-4 col-md-4">
<button @click.prevent="testNotifier('success')" class="btn btn-outline-dark btn-block text-capitalize test-notifier">
<i class="fa fa-vial"></i>{{loadingTest ? "Loading..." : "Test Success"}}</button>
</div>
<div class="col-4 col-md-4">
<button @click.prevent="testNotifier('failure')" class="btn btn-outline-dark btn-block text-capitalize test-notifier">
<i class="fa fa-vial"></i>{{loadingTest ? "Loading..." : "Test Failure"}}</button>
</div>
</div>
@ -69,28 +109,81 @@
</template>
<script>
import Api from "../API";
import Api from "../API";
const beautify = require('js-beautify').js
// require component
import { codemirror } from 'vue-codemirror'
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/neat.css'
import '../codemirror_json'
export default {
name: 'Notifier',
components: {
codemirror
},
props: {
notifier: {
type: Object,
required: true
}
},
watch: {
},
data() {
return {
loading: false,
loadingTest: false,
error: null,
response: null,
request: null,
success: false,
saved: false,
success_data: null,
failure_data: null,
form: {},
cmOptions: {
height: 700,
tabSize: 2,
lineNumbers: true,
line: true,
class: "json-field",
theme: 'neat',
mode: "mymode",
lineWrapping: true,
json: true,
autoRefresh: true,
mime: this.notifier.data_type === "json" ? "application/json" : "text/plain"
},
beautifySettings: { indent_size: 2, space_in_empty_paren: true },
}
},
computed: {
},
methods: {
visible(isVisible, entry) {
if (isVisible) {
this.$refs.cmfailure.codemirror.refresh()
this.$refs.cmsuccess.codemirror.refresh()
}
},
onCmSuccessReady(cm) {
this.success_data = beautify(this.notifier.success_data, this.beautifySettings)
setTimeout(function() {
cm.refresh();
},1);
},
onCmFailureReady(cm) {
this.failure_data = beautify(this.notifier.failure_data, this.beautifySettings)
setTimeout(function() {
cm.refresh();
},1);
},
async enableToggle() {
this.notifier.enabled = !!this.notifier.enabled
const form = {
@ -112,13 +205,15 @@ export default {
}
this.form[field] = val
});
this.form.success_data = this.success_data
this.form.failure_data = this.failure_data
await Api.notifier_save(this.form)
const notifiers = await Api.notifiers()
await this.$store.commit('setNotifiers', notifiers)
this.saved = true
this.loading = false
},
async testNotifier() {
async testNotifier(method="success") {
this.success = false
this.loadingTest = true
this.form.method = this.notifier.method
@ -130,7 +225,11 @@ export default {
}
this.form[field] = val
});
const tested = await Api.notifier_test(this.form)
let req = {
notifier: this.form,
method: method,
}
const tested = await Api.notifier_test(req, this.notifier.method)
if (tested.success) {
this.success = true
} else {
@ -142,7 +241,10 @@ export default {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.CodeMirror {
border: 1px solid #eee;
height: 550px;
font-size: 9pt;
}
</style>

View File

@ -1,17 +1,17 @@
<template>
<form v-if="service.type" @submit.prevent="saveService">
<div class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Service Information</div>
<div class="card-header">{{ $t('service.info') }}</div>
<div class="card-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Service Name</label>
<label class="col-sm-4 col-form-label">{{ $t('service.name') }}</label>
<div class="col-sm-8">
<input v-model="service.name" @input="updatePermalink" id="name" type="text" name="name" class="form-control" placeholder="Server Name" required spellcheck="false" autocorrect="off">
<small class="form-text text-muted">Give your service a name you can recognize</small>
</div>
</div>
<div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Service Type</label>
<label for="service_type" class="col-sm-4 col-form-label">{{ $t('service.type') }}</label>
<div class="col-sm-8">
<select v-model="service.type" class="form-control" id="service_type">
<option value="http">HTTP Service</option>
@ -24,7 +24,7 @@
</div>
</div>
<div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Group</label>
<label for="service_type" class="col-sm-4 col-form-label">{{ $t('group') }}</label>
<div class="col-sm-8">
<select v-model="service.group_id" class="form-control">
<option value="0" >No Group</option>

View File

@ -26,13 +26,19 @@
<option value="mysql">MySQL</option>
</select>
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('setup.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 class="text-capitalize">{{ $t('port') }}</label>
<input @keyup="canSubmit" v-model="setup.db_port" id="db_port" type="text" class="form-control" placeholder="localhost">
<div class="row">
<div class="col-6">
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('setup.host') }}</label>
<input @keyup="canSubmit" v-model="setup.db_host" id="db_host" type="text" class="form-control" placeholder="localhost">
</div>
</div>
<div class="col-6">
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('port') }}</label>
<input @keyup="canSubmit" v-model="setup.db_port" id="db_port" type="number" class="form-control" placeholder="5432">
</div>
</div>
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group">
<label class="text-capitalize">{{ $t('username') }}</label>
@ -47,6 +53,21 @@
<input @keyup="canSubmit" v-model="setup.db_database" id="db_database" type="text" class="form-control" placeholder="Database name">
</div>
<div class="form-group mt-3">
<div class="row">
<div class="col-9">
<span class="text-left text-capitalize">{{ $t('setup.send_reports') }}</span>
</div>
<div class="col-3 text-right">
<span @click="setup.send_reports = !!setup.send_reports" class="switch">
<input v-model="setup.send_reports" type="checkbox" name="send_reports" class="switch" id="send_reports" :checked="setup.send_reports">
<label for="send_reports"></label>
</span>
</div>
</div>
</div>
</div>
<div class="col-6">
@ -72,7 +93,7 @@
</div>
<div class="form-group">
<label class="text-capitalize">{{ $t('setup.username') }}</label>
<label class="text-capitalize">{{ $t('setup.password') }}</label>
<input @keyup="canSubmit" v-model="setup.password" id="password" type="password" class="form-control" placeholder="password" required>
</div>
@ -90,7 +111,7 @@
<div class="col-4">
<label class="d-none d-sm-block text-capitalize text-capitalize">{{ $t('setup.newsletter') }}</label>
<span @click="setup.newsletter = !!setup.newsletter" class="switch">
<input v-model="setup.newsletter" type="checkbox" name="using_cdn" class="switch" id="send_newsletter" :checked="setup.newsletter">
<input v-model="setup.newsletter" type="checkbox" name="send_newsletter" class="switch" id="send_newsletter" :checked="setup.newsletter">
<label for="send_newsletter"></label>
</span>
</div>
@ -140,6 +161,7 @@
confirm_password: "",
sample_data: true,
newsletter: true,
send_reports: true,
email: "",
}
}

View File

@ -1,4 +1,13 @@
const english = {
top_nav: {
dashboard: "Dashboard",
services: "Services",
users: "Users",
announcements: "Announcements",
settings: "Settings",
logs: "Logs",
logout: 'Logout',
},
setup: {
language: "Language",
connection: "Database Connection",
@ -9,25 +18,54 @@ const english = {
domain: "Domain URL",
username: "Admin Username",
password: "Admin Password",
password_confirm: "Confirm Admin Username",
password_confirm: "Confirm Admin Password",
newsletter: "Newsletter",
newsletter_note: "We will not share your email, emails are only for major updates.",
send_reports: "Send Error Reports to Statping"
},
dashboard: {
total_services: "Total Services",
failures_24_hours: "Failures last 24 Hours",
online_services: "Online Services",
},
settings: {
name: "Project Name",
description: "Project Name",
footer: "Custom Footer",
footer_notes: "HTML is allowed inside the footer",
error_reporting: "Enable Error Reporting",
error_reporting_notes: "Help the Statping project out by sending anonymous error logs back to our server.",
save: "Save Settings"
},
service: {
name: "Service Name",
type: "Service Type",
info: "Service Information"
},
email: "Email Address",
port: "Database Port",
setting: "Settings",
username: "Username",
password: 'password',
service: 'service',
password: 'Password',
services: 'Services',
domain: 'Domain',
online: 'online',
public: 'Public',
private: 'Private',
admin: 'Admin',
user: 'User',
offline: 'offline',
incident: 'incident',
group: 'group',
group: 'Group',
message: 'message',
logout: 'logout',
edit: 'Edit',
type: 'Type',
sample_data: 'Sample Data',
today: 'Today',
last_login: 'Last Login',
day: 'day',
hour: 'hour',
uptime: '{0} Uptime',
}
export default english

View File

@ -8,9 +8,7 @@
<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>
<span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
<GroupServiceFailures :service="service"/>
<IncidentsBlock :service="service"/>
</a>
</div>

View File

@ -21,7 +21,7 @@
<h6 class="mt-4 text-muted">Notifiers</h6>
<div id="notifiers_tabs">
<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">
<a v-for="(notifier, index) in notifiers" v-bind:key="`${notifier.method}`" @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>

View File

@ -6679,7 +6679,7 @@ js-base64@^2.1.8:
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.2.tgz#313b6274dda718f714d00b3330bbae6e38e90209"
integrity sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==
js-beautify@^1.6.12:
js-beautify@^1.11.0, js-beautify@^1.6.12:
version "1.11.0"
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.11.0.tgz#afb873dc47d58986360093dcb69951e8bcd5ded2"
integrity sha512-a26B+Cx7USQGSWnz9YxgJNMmML/QG2nqIaL7VVYPCXbqiKz8PN0waSNvroMtvAK6tY7g/wPdNWGEP+JTNIBr6A==

1
go.mod
View File

@ -14,6 +14,7 @@ require (
github.com/go-mail/mail v2.3.1+incompatible
github.com/golang/protobuf v1.3.5 // indirect
github.com/gorilla/mux v1.7.4
github.com/gorilla/securecookie v1.1.1
github.com/jinzhu/gorm v1.9.12
github.com/magiconair/properties v1.8.1
github.com/mattn/go-sqlite3 v2.0.3+incompatible

2
go.sum
View File

@ -123,6 +123,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=

View File

@ -250,7 +250,7 @@ func TestMainApiRoutes(t *testing.T) {
`go_memstats_alloc_bytes`,
`http_duration_seconds_count`,
`http_response_bytes_count`,
`service_request_duration_count`,
`service_success`,
},
},
}

View File

@ -3,14 +3,12 @@ package handlers
import (
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/statping/statping/source"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/users"
"github.com/statping/statping/utils"
"net/http"
"os"
"time"
)
func logoutHandler(w http.ResponseWriter, r *http.Request) {
@ -132,44 +130,6 @@ func logsLineHandler(w http.ResponseWriter, r *http.Request) {
}
}
type JwtClaim struct {
Username string `json:"username"`
Admin bool `json:"admin"`
jwt.StandardClaims
}
func removeJwtToken(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: cookieKey,
Value: "",
Expires: time.Now(),
MaxAge: -1,
})
}
func setJwtToken(user *users.User, w http.ResponseWriter) (JwtClaim, string) {
expirationTime := time.Now().Add(72 * time.Hour)
jwtClaim := JwtClaim{
Username: user.Username,
Admin: user.Admin.Bool,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
}}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtClaim)
tokenString, err := token.SignedString([]byte(jwtKey))
if err != nil {
log.Errorln("error setting token: ", err)
}
user.Token = tokenString
// set cookies
http.SetCookie(w, &http.Cookie{
Name: cookieKey,
Value: tokenString,
Expires: expirationTime,
})
return jwtClaim, tokenString
}
func apiLoginHandler(w http.ResponseWriter, r *http.Request) {
form := parseForm(r)
username := form.Get("username")

View File

@ -3,7 +3,6 @@ package handlers
import (
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/statping/statping/types/errors"
"html/template"
"net/http"
@ -15,8 +14,8 @@ import (
)
const (
cookieKey = "statping_auth"
timeout = time.Second * 30
cookieName = "statping_auth"
timeout = time.Second * 30
)
var (
@ -100,31 +99,6 @@ func IsFullAuthenticated(r *http.Request) bool {
return IsAdmin(r)
}
func getJwtToken(r *http.Request) (JwtClaim, error) {
c, err := r.Cookie(cookieKey)
if err != nil {
if err == http.ErrNoCookie {
return JwtClaim{}, err
}
return JwtClaim{}, err
}
tknStr := c.Value
var claims JwtClaim
tkn, err := jwt.ParseWithClaims(tknStr, &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtKey), nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
return JwtClaim{}, err
}
return JwtClaim{}, err
}
if !tkn.Valid {
return claims, errors.New("token is not valid")
}
return claims, err
}
// ScopeName will show private JSON fields in the API.
// It will return "admin" if request has valid admin authentication.
func ScopeName(r *http.Request) string {

74
handlers/jwt.go Normal file
View File

@ -0,0 +1,74 @@
package handlers
import (
"github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
"github.com/statping/statping/types/users"
"net/http"
"time"
)
type JwtClaim struct {
Username string `json:"username"`
Admin bool `json:"admin"`
jwt.StandardClaims
}
func removeJwtToken(w http.ResponseWriter) {
c := http.Cookie{
Name: cookieName,
Value: "",
MaxAge: -1,
Path: "/",
}
http.SetCookie(w, &c)
}
func setJwtToken(user *users.User, w http.ResponseWriter) (JwtClaim, string) {
expirationTime := time.Now().Add(72 * time.Hour)
jwtClaim := JwtClaim{
Username: user.Username,
Admin: user.Admin.Bool,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
}}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtClaim)
tokenString, err := token.SignedString([]byte(jwtKey))
if err != nil {
log.Errorln("error setting token: ", err)
}
user.Token = tokenString
// set cookies
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: tokenString,
Expires: expirationTime,
MaxAge: int(time.Duration(72 * time.Hour).Seconds()),
})
return jwtClaim, tokenString
}
func getJwtToken(r *http.Request) (JwtClaim, error) {
c, err := r.Cookie(cookieName)
if err != nil {
if err == http.ErrNoCookie {
return JwtClaim{}, err
}
return JwtClaim{}, err
}
tknStr := c.Value
var claims JwtClaim
tkn, err := jwt.ParseWithClaims(tknStr, &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtKey), nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
return JwtClaim{}, err
}
return JwtClaim{}, err
}
if !tkn.Valid {
return claims, errors.New("token is not valid")
}
return claims, err
}

View File

@ -190,6 +190,5 @@ func DecodeJSON(r *http.Request, obj interface{}) error {
if err != nil {
return errors.DecodeJSON
}
defer r.Body.Close()
return nil
return r.Body.Close()
}

View File

@ -2,6 +2,7 @@ package handlers
import (
"github.com/gorilla/mux"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/services"
"net/http"
@ -45,6 +46,8 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
return
}
log.Infof("Updating %s Notifier", notifer.Title)
err = notifer.Update()
if err != nil {
sendErrorJson(err, w, r)
@ -54,21 +57,33 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
sendJsonAction(vars["notifier"], "update", w, r)
}
type testNotificationReq struct {
Method string `json:"method"`
Notification notifications.Notification `json:"notifier"`
}
func testNotificationHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
notifer, err := notifications.Find(vars["notifier"])
_, err := notifications.Find(vars["notifier"])
if err != nil {
sendErrorJson(err, w, r)
return
}
if err := DecodeJSON(r, &notifer); err != nil {
var req testNotificationReq
if err := DecodeJSON(r, &req); err != nil {
sendErrorJson(err, w, r)
return
}
notif := services.ReturnNotifier(notifer.Method)
out, err := notif.OnTest()
notif := services.ReturnNotifier(req.Notification.Method)
var out string
if req.Method == "success" {
out, err = notif.OnSuccess(services.Example(true))
} else {
out, err = notif.OnFailure(services.Example(false), failures.Example())
}
resp := &notifierTestResp{
Success: err == nil,

View File

@ -73,8 +73,8 @@ func Router() *mux.Router {
r.PathPrefix("/css/").Handler(http.StripPrefix(basePath, Gzip(staticAssets("css"))))
r.PathPrefix("/js/").Handler(http.StripPrefix(basePath, Gzip(staticAssets("js"))))
r.PathPrefix("/scss/").Handler(http.StripPrefix(basePath, Gzip(staticAssets("scss"))))
r.PathPrefix("/favicon/").Handler(http.StripPrefix(basePath, Gzip(staticAssets("favicon"))))
r.PathPrefix("/robots.txt").Handler(http.StripPrefix(basePath, indexHandler))
r.PathPrefix("/favicon.ico").Handler(http.StripPrefix(basePath, indexHandler))
r.PathPrefix("/banner.png").Handler(http.StripPrefix(basePath, indexHandler))
} else {
tmplFileSrv := http.FileServer(source.TmplBox.HTTPBox())
@ -83,8 +83,8 @@ func Router() *mux.Router {
r.PathPrefix("/css/").Handler(http.StripPrefix(basePath, Gzip(tmplFileSrv)))
r.PathPrefix("/scss/").Handler(http.StripPrefix(basePath, Gzip(tmplFileSrv)))
r.PathPrefix("/js/").Handler(http.StripPrefix(basePath, Gzip(tmplFileSrv)))
r.PathPrefix("/favicon/").Handler(http.StripPrefix(basePath, Gzip(tmplFileSrv)))
r.PathPrefix("/robots.txt").Handler(tmplBoxHandler)
r.PathPrefix("/favicon.ico").Handler(tmplBoxHandler)
r.PathPrefix("/banner.png").Handler(tmplBoxHandler)
}

View File

@ -34,6 +34,8 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
domain := r.PostForm.Get("domain")
newsletter := r.PostForm.Get("newsletter")
sendNews, _ := strconv.ParseBool(newsletter)
reports := r.PostForm.Get("send_reports")
sendReports, _ := strconv.ParseBool(reports)
log.WithFields(utils.ToFields(core.App, confgs)).Debugln("new configs posted")
@ -81,16 +83,17 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
notifiers.InitNotifiers()
c := &core.Core{
Name: project,
Description: description,
ApiSecret: utils.Params.GetString("API_SECRET"),
Domain: domain,
Version: core.App.Version,
Started: utils.Now(),
CreatedAt: utils.Now(),
UseCdn: null.NewNullBool(false),
Footer: null.NewNullString(""),
Language: confgs.Language,
Name: project,
Description: description,
ApiSecret: utils.Params.GetString("API_SECRET"),
Domain: domain,
Version: core.App.Version,
Started: utils.Now(),
CreatedAt: utils.Now(),
UseCdn: null.NewNullBool(false),
Footer: null.NewNullString(""),
Language: confgs.Language,
AllowReports: null.NewNullBool(sendReports),
}
log.Infoln("Creating new Core")

View File

@ -29,6 +29,9 @@ var Command = &commandLine{&notifications.Notification{
Delay: time.Duration(1 * time.Second),
Icon: "fas fa-terminal",
Host: "/bin/bash",
SuccessData: "curl -L http://localhost:8080",
FailureData: "curl -L http://localhost:8080",
DataType: "text",
Limits: 60,
Form: []notifications.NotificationForm{{
Type: "text",
@ -36,18 +39,6 @@ var Command = &commandLine{&notifications.Notification{
Placeholder: "/usr/bin/curl",
DbField: "host",
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: "http://localhost:8080/health",
DbField: "var1",
SmallText: "<b>Accepts Variables</b> This Command will run when a service is receiving a Successful event.",
}, {
Type: "text",
Title: "Command to Run on OnFailure",
Placeholder: "http://localhost:8080/health",
DbField: "var2",
SmallText: "<b>Accepts Variables</b> This Command will run when a service is receiving a Failing event.",
}}},
}
@ -58,24 +49,22 @@ func runCommand(app string, cmd ...string) (string, string, error) {
}
// OnSuccess for commandLine will trigger successful service
func (c *commandLine) OnSuccess(s *services.Service) error {
msg := c.GetValue("var1")
tmpl := ReplaceVars(msg, s, nil)
_, _, err := runCommand(c.Host, tmpl)
return err
func (c *commandLine) OnSuccess(s *services.Service) (string, error) {
tmpl := ReplaceVars(c.SuccessData, s, nil)
out, _, err := runCommand(c.Host, tmpl)
return out, 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
func (c *commandLine) OnFailure(s *services.Service, f *failures.Failure) (string, error) {
tmpl := ReplaceVars(c.FailureData, s, f)
_, ouerr, err := runCommand(c.Host, tmpl)
return ouerr, err
}
// OnTest for commandLine triggers when this notifier has been saved
func (c *commandLine) OnTest() (string, error) {
tmpl := ReplaceVars(c.Var1, exampleService, exampleFailure)
tmpl := ReplaceVars(c.Var1, services.Example(true), exampleFailure)
in, out, err := runCommand(c.Host, tmpl)
utils.Log.Infoln(in)
utils.Log.Infoln(out)

View File

@ -39,12 +39,12 @@ func TestCommandNotifier(t *testing.T) {
})
t.Run("Command OnFailure", func(t *testing.T) {
err := Command.OnFailure(exampleService, exampleFailure)
_, err := Command.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
})
t.Run("Command OnSuccess", func(t *testing.T) {
err := Command.OnSuccess(exampleService)
_, err := Command.OnSuccess(exampleService)
assert.Nil(t, err)
})

View File

@ -28,6 +28,9 @@ var Discorder = &discord{&notifications.Notification{
Delay: time.Duration(5 * time.Second),
Host: "https://discordapp.com/api/webhooks/****/*****",
Icon: "fab fa-discord",
SuccessData: `{"content": "Your service '{{.Service.Name}}' is currently online!"}`,
FailureData: `{"content": "Your service '{{.Service.Name}}' is currently failing! Reason: {{.Failure.Issue}}"}`,
DataType: "json",
Limits: 60,
Form: []notifications.NotificationForm{{
Type: "text",
@ -38,9 +41,9 @@ var Discorder = &discord{&notifications.Notification{
}
// Send will send a HTTP Post to the discord API. It accepts type: []byte
func (d *discord) sendRequest(msg string) error {
_, _, err := utils.HttpRequest(Discorder.GetValue("host"), "POST", "application/json", nil, strings.NewReader(msg), time.Duration(10*time.Second), true, nil)
return err
func (d *discord) sendRequest(msg string) (string, error) {
out, _, err := utils.HttpRequest(Discorder.GetValue("host"), "POST", "application/json", nil, strings.NewReader(msg), time.Duration(10*time.Second), true, nil)
return string(out), err
}
func (d *discord) Select() *notifications.Notification {
@ -48,15 +51,17 @@ func (d *discord) Select() *notifications.Notification {
}
// OnFailure will trigger failing service
func (d *discord) OnFailure(s *services.Service, f *failures.Failure) error {
func (d *discord) OnFailure(s *services.Service, f *failures.Failure) (string, error) {
msg := `{"content": "Your service '{{.Service.Name}}' is currently failing! Reason: {{.Failure.Issue}}"}`
return d.sendRequest(ReplaceVars(msg, s, f))
out, err := d.sendRequest(ReplaceVars(msg, s, f))
return out, err
}
// OnSuccess will trigger successful service
func (d *discord) OnSuccess(s *services.Service) error {
func (d *discord) OnSuccess(s *services.Service) (string, error) {
msg := `{"content": "Your service '{{.Service.Name}}' is currently online!"}`
return d.sendRequest(ReplaceVars(msg, s, nil))
out, err := d.sendRequest(ReplaceVars(msg, s, nil))
return out, err
}
// OnSave triggers when this notifier has been saved

View File

@ -46,12 +46,12 @@ func TestDiscordNotifier(t *testing.T) {
})
t.Run("discord OnFailure", func(t *testing.T) {
err := Discorder.OnFailure(exampleService, exampleFailure)
_, err := Discorder.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
})
t.Run("discord OnSuccess", func(t *testing.T) {
err := Discorder.OnSuccess(exampleService)
_, err := Discorder.OnSuccess(exampleService)
assert.Nil(t, err)
})

View File

@ -110,6 +110,9 @@ var email = &emailer{&notifications.Notification{
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Icon: "far fa-envelope",
SuccessData: "Service {{.Service.Name}} is Back Online",
FailureData: "Service {{.Service.Name}} is Offline",
DataType: "text",
Limits: 30,
Form: []notifications.NotificationForm{{
Type: "text",
@ -161,7 +164,7 @@ type emailOutgoing struct {
}
// OnFailure will trigger failing service
func (e *emailer) OnFailure(s *services.Service, f *failures.Failure) error {
func (e *emailer) OnFailure(s *services.Service, f *failures.Failure) (string, error) {
subject := fmt.Sprintf("Service %s is Offline", s.Name)
email := &emailOutgoing{
To: e.Var2,
@ -173,11 +176,11 @@ func (e *emailer) OnFailure(s *services.Service, f *failures.Failure) error {
},
From: e.Var1,
}
return e.dialSend(email)
return "email failed", e.dialSend(email)
}
// OnSuccess will trigger successful service
func (e *emailer) OnSuccess(s *services.Service) error {
func (e *emailer) OnSuccess(s *services.Service) (string, error) {
subject := fmt.Sprintf("Service %s is Back Online", s.Name)
email := &emailOutgoing{
To: e.Var2,
@ -189,7 +192,7 @@ func (e *emailer) OnSuccess(s *services.Service) error {
},
From: e.Var1,
}
return e.dialSend(email)
return "email sent", e.dialSend(email)
}
// OnTest triggers when this notifier has been saved
@ -232,6 +235,7 @@ func (e *emailer) dialSend(email *emailOutgoing) error {
} else {
mailer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
m.SetHeader("From", email.From)
m.SetHeader("To", email.To)
m.SetHeader("Subject", email.Subject)

View File

@ -62,12 +62,12 @@ func TestEmailNotifier(t *testing.T) {
})
t.Run("email OnFailure", func(t *testing.T) {
err := email.OnFailure(exampleService, exampleFailure)
_, err := email.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
})
t.Run("email OnSuccess", func(t *testing.T) {
err := email.OnSuccess(exampleService)
_, err := email.OnSuccess(exampleService)
assert.Nil(t, err)
})
@ -76,7 +76,7 @@ func TestEmailNotifier(t *testing.T) {
})
t.Run("email OnSuccess Again", func(t *testing.T) {
err := email.OnSuccess(exampleService)
_, err := email.OnSuccess(exampleService)
assert.Nil(t, err)
})

View File

@ -52,17 +52,17 @@ func (l *lineNotifier) sendMessage(message string) (string, error) {
}
// OnFailure will trigger failing service
func (l *lineNotifier) OnFailure(s *services.Service, f *failures.Failure) error {
func (l *lineNotifier) OnFailure(s *services.Service, f *failures.Failure) (string, error) {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
_, err := l.sendMessage(msg)
return err
out, err := l.sendMessage(msg)
return out, err
}
// OnSuccess will trigger successful service
func (l *lineNotifier) OnSuccess(s *services.Service) error {
func (l *lineNotifier) OnSuccess(s *services.Service) (string, error) {
msg := fmt.Sprintf("Service %s is online!", s.Name)
_, err := l.sendMessage(msg)
return err
out, err := l.sendMessage(msg)
return out, err
}
// OnTest triggers when this notifier has been saved

View File

@ -71,18 +71,18 @@ func dataJson(s *services.Service, f *failures.Failure) map[string]interface{} {
}
// OnFailure will trigger failing service
func (m *mobilePush) OnFailure(s *services.Service, f *failures.Failure) error {
func (m *mobilePush) OnFailure(s *services.Service, f *failures.Failure) (string, error) {
data := dataJson(s, f)
msg := &pushArray{
Message: fmt.Sprintf("Your service '%v' is currently failing! Reason: %v", s.Name, f.Issue),
Title: "Service Offline",
Data: data,
}
return m.Send(msg)
return "notification sent", m.Send(msg)
}
// OnSuccess will trigger successful service
func (m *mobilePush) OnSuccess(s *services.Service) error {
func (m *mobilePush) OnSuccess(s *services.Service) (string, error) {
data := dataJson(s, nil)
msg := &pushArray{
Message: "Service is Online!",
@ -90,7 +90,7 @@ func (m *mobilePush) OnSuccess(s *services.Service) error {
Data: data,
Platform: 2,
}
return m.Send(msg)
return "notification sent", m.Send(msg)
}
// OnTest triggers when this notifier has been saved

View File

@ -51,12 +51,12 @@ func TestMobileNotifier(t *testing.T) {
})
t.Run("Mobile OnFailure", func(t *testing.T) {
err := Mobile.OnFailure(exampleService, exampleFailure)
_, err := Mobile.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
})
t.Run("Mobile OnSuccess", func(t *testing.T) {
err := Mobile.OnSuccess(exampleService)
_, err := Mobile.OnSuccess(exampleService)
assert.Nil(t, err)
})

View File

@ -12,6 +12,7 @@ import (
)
var log = utils.Log.WithField("type", "notifier")
var exampleService = services.Example(true)
type replacer struct {
Core *core.Core
@ -62,7 +63,7 @@ func ReplaceVars(input string, s *services.Service, f *failures.Failure) string
return ReplaceTemplate(input, replacer{Service: s, Failure: f, Core: core.App})
}
var exampleService = &services.Service{
var ExampleService = &services.Service{
Id: 1,
Name: "Statping",
Domain: "https://statping.com",

View File

@ -2,15 +2,17 @@ package notifiers
import (
"github.com/magiconair/properties/assert"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/services"
"testing"
)
func TestReplaceTemplate(t *testing.T) {
temp := `{"id":{{.Service.Id}},"name":"{{.Service.Name}}"}`
replaced := ReplaceTemplate(temp, replacer{Service: exampleService})
assert.Equal(t, `{"id":1,"name":"Statping"}`, replaced)
replaced := ReplaceTemplate(temp, replacer{Service: services.Example(true)})
assert.Equal(t, `{"id":6283,"name":"Statping Example"}`, replaced)
temp = `{"id":{{.Service.Id}},"name":"{{.Service.Name}}","failure":"{{.Failure.Issue}}"}`
replaced = ReplaceTemplate(temp, replacer{Service: exampleService, Failure: exampleFailure})
assert.Equal(t, `{"id":1,"name":"Statping","failure":"HTTP returned a 500 status code"}`, replaced)
replaced = ReplaceTemplate(temp, replacer{Service: services.Example(false), Failure: failures.Example()})
assert.Equal(t, `{"id":6283,"name":"Statping Example","failure":"Response did not response a 200 status code"}`, replaced)
}

View File

@ -35,6 +35,9 @@ var Pushover = &pushover{&notifications.Notification{
Icon: "fa dot-circle",
Delay: time.Duration(10 * time.Second),
Limits: 60,
SuccessData: `Your service '{{.Service.Name}}' is currently online!`,
FailureData: `Your service '{{.Service.Name}}' is currently offline!`,
DataType: "text",
Form: []notifications.NotificationForm{{
Type: "text",
Title: "User Token",
@ -67,22 +70,23 @@ func (t *pushover) sendMessage(message string) (string, error) {
}
// OnFailure will trigger failing service
func (t *pushover) OnFailure(s *services.Service, f *failures.Failure) error {
msg := fmt.Sprintf("Your service '%s' is currently offline!", s.Name)
_, err := t.sendMessage(msg)
return err
func (t *pushover) OnFailure(s *services.Service, f *failures.Failure) (string, error) {
message := ReplaceVars(t.FailureData, s, f)
out, err := t.sendMessage(message)
return out, err
}
// OnSuccess will trigger successful service
func (t *pushover) OnSuccess(s *services.Service) error {
msg := fmt.Sprintf("Your service '%s' is currently online!", s.Name)
_, err := t.sendMessage(msg)
return err
func (t *pushover) OnSuccess(s *services.Service) (string, error) {
message := ReplaceVars(t.SuccessData, s, nil)
out, err := t.sendMessage(message)
return out, err
}
// OnTest will test the Pushover SMS messaging
func (t *pushover) OnTest() (string, error) {
msg := fmt.Sprintf("Testing the Pushover Notifier, Your service '%s' is currently offline! Error: %s", exampleService.Name, exampleFailure.Issue)
example := services.Example(true)
msg := fmt.Sprintf("Testing the Pushover Notifier, Your service '%s' is currently offline! Error: %s", example.Name, exampleFailure.Issue)
content, err := t.sendMessage(msg)
return content, err
}

View File

@ -49,12 +49,12 @@ func TestPushoverNotifier(t *testing.T) {
})
t.Run("Pushover OnFailure", func(t *testing.T) {
err := Pushover.OnFailure(exampleService, exampleFailure)
_, err := Pushover.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
})
t.Run("Pushover OnSuccess", func(t *testing.T) {
err := Pushover.OnSuccess(exampleService)
_, err := Pushover.OnSuccess(exampleService)
assert.Nil(t, err)
})

View File

@ -15,7 +15,10 @@ import (
var _ notifier.Notifier = (*slack)(nil)
const (
slackMethod = "slack"
slackMethod = "slack"
)
var (
failingTemplate = `{ "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": ":warning: The service {{.Service.Name}} is currently offline! :warning:" } }, { "type": "divider" }, { "type": "section", "fields": [ { "type": "mrkdwn", "text": "*Service:*\n{{.Service.Name}}" }, { "type": "mrkdwn", "text": "*URL:*\n{{.Service.Domain}}" }, { "type": "mrkdwn", "text": "*Status Code:*\n{{.Service.LastStatusCode}}" }, { "type": "mrkdwn", "text": "*When:*\n{{.Failure.CreatedAt}}" }, { "type": "mrkdwn", "text": "*Downtime:*\n{{.Service.DowntimeAgo}}" }, { "type": "plain_text", "text": "*Error:*\n{{.Failure.Issue}}" } ] }, { "type": "divider" }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "View Offline Service", "emoji": true }, "style": "danger", "url": "{{.Core.Domain}}/service/{{.Service.Id}}" }, { "type": "button", "text": { "type": "plain_text", "text": "Go to Statping", "emoji": true }, "url": "{{.Core.Domain}}" } ] } ] }`
successTemplate = `{ "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "The service {{.Service.Name}} is back online." } }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "View Service", "emoji": true }, "style": "primary", "url": "{{.Core.Domain}}/service/{{.Service.Id}}" }, { "type": "button", "text": { "type": "plain_text", "text": "Go to Statping", "emoji": true }, "url": "{{.Core.Domain}}" } ] } ] }`
)
@ -37,6 +40,10 @@ var slacker = &slack{&notifications.Notification{
Delay: time.Duration(10 * time.Second),
Host: "https://webhooksurl.slack.com/***",
Icon: "fab fa-slack",
SuccessData: successTemplate,
FailureData: failingTemplate,
DataType: "json",
RequestInfo: "Slack allows you to customize your own messages with many complex components. Checkout the <a target=\"_blank\" href=\"https://api.slack.com/reference/surfaces/formatting\">Slack Message API</a> to learn how you can create your own.",
Limits: 60,
Form: []notifications.NotificationForm{{
Type: "text",
@ -49,17 +56,17 @@ var slacker = &slack{&notifications.Notification{
}
// Send will send a HTTP Post to the slack webhooker API. It accepts type: string
func (s *slack) sendSlack(msg string) error {
_, resp, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, strings.NewReader(msg), time.Duration(10*time.Second), true, nil)
func (s *slack) sendSlack(msg string) (string, error) {
resp, _, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, strings.NewReader(msg), time.Duration(10*time.Second), true, nil)
if err != nil {
return err
return "", err
}
defer resp.Body.Close()
return nil
return string(resp), nil
}
func (s *slack) OnTest() (string, error) {
testMsg := ReplaceVars(failingTemplate, exampleService, exampleFailure)
example := services.Example(true)
testMsg := ReplaceVars(failingTemplate, example, nil)
contents, resp, err := utils.HttpRequest(s.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(testMsg)), time.Duration(10*time.Second), true, nil)
if err != nil {
return "", err
@ -72,13 +79,15 @@ func (s *slack) OnTest() (string, error) {
}
// OnFailure will trigger failing service
func (s *slack) OnFailure(srv *services.Service, f *failures.Failure) error {
func (s *slack) OnFailure(srv *services.Service, f *failures.Failure) (string, error) {
msg := ReplaceVars(failingTemplate, srv, f)
return s.sendSlack(msg)
out, err := s.sendSlack(msg)
return out, err
}
// OnSuccess will trigger successful service
func (s *slack) OnSuccess(srv *services.Service) error {
func (s *slack) OnSuccess(srv *services.Service) (string, error) {
msg := ReplaceVars(successTemplate, srv, nil)
return s.sendSlack(msg)
out, err := s.sendSlack(msg)
return out, err
}

View File

@ -47,12 +47,12 @@ func TestSlackNotifier(t *testing.T) {
})
t.Run("slack OnFailure", func(t *testing.T) {
err := slacker.OnFailure(exampleService, exampleFailure)
_, err := slacker.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
})
t.Run("slack OnSuccess", func(t *testing.T) {
err := slacker.OnSuccess(exampleService)
_, err := slacker.OnSuccess(exampleService)
assert.Nil(t, err)
})

View File

@ -32,6 +32,9 @@ var Telegram = &telegram{&notifications.Notification{
AuthorUrl: "https://github.com/hunterlong",
Icon: "fab fa-telegram-plane",
Delay: time.Duration(5 * time.Second),
SuccessData: "Your service '{{.Service.Name}}' is currently online!",
FailureData: "Your service '{{.Service.Name}}' is currently offline!",
DataType: "text",
Limits: 60,
Form: []notifications.NotificationForm{{
Type: "text",
@ -71,17 +74,17 @@ func (t *telegram) sendMessage(message string) (string, error) {
}
// 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)
_, err := t.sendMessage(msg)
return err
func (t *telegram) OnFailure(s *services.Service, f *failures.Failure) (string, error) {
msg := ReplaceVars(t.FailureData, s, f)
out, err := t.sendMessage(msg)
return out, 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)
_, err := t.sendMessage(msg)
return err
func (t *telegram) OnSuccess(s *services.Service) (string, error) {
msg := ReplaceVars(t.SuccessData, s, nil)
out, err := t.sendMessage(msg)
return out, err
}
// OnTest will test the Twilio SMS messaging

View File

@ -53,12 +53,12 @@ func TestTelegramNotifier(t *testing.T) {
})
t.Run("Telegram OnFailure", func(t *testing.T) {
err := Telegram.OnFailure(exampleService, exampleFailure)
_, err := Telegram.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
})
t.Run("Telegram OnSuccess", func(t *testing.T) {
err := Telegram.OnSuccess(exampleService)
_, err := Telegram.OnSuccess(exampleService)
assert.Nil(t, err)
})

View File

@ -33,6 +33,9 @@ var Twilio = &twilio{&notifications.Notification{
AuthorUrl: "https://github.com/hunterlong",
Icon: "far fa-comment-alt",
Delay: time.Duration(10 * time.Second),
SuccessData: "Your service '{{.Service.Name}}' is currently online!",
FailureData: "Your service '{{.Service.Name}}' is currently offline!",
DataType: "text",
Limits: 15,
Form: []notifications.NotificationForm{{
Type: "text",
@ -87,17 +90,15 @@ func (t *twilio) sendMessage(message string) (string, error) {
}
// 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)
_, err := t.sendMessage(msg)
return err
func (t *twilio) OnFailure(s *services.Service, f *failures.Failure) (string, error) {
msg := ReplaceVars(t.FailureData, s, f)
return t.sendMessage(msg)
}
// OnSuccess will trigger successful service
func (t *twilio) OnSuccess(s *services.Service) error {
msg := fmt.Sprintf("Your service '%v' is currently online!", s.Name)
_, err := t.sendMessage(msg)
return err
func (t *twilio) OnSuccess(s *services.Service) (string, error) {
msg := ReplaceVars(t.SuccessData, s, nil)
return t.sendMessage(msg)
}
// OnTest will test the Twilio SMS messaging

View File

@ -53,12 +53,12 @@ func TestTwilioNotifier(t *testing.T) {
})
t.Run("Twilio OnFailure", func(t *testing.T) {
err := Twilio.OnFailure(exampleService, exampleFailure)
_, err := Twilio.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
})
t.Run("Twilio OnSuccess", func(t *testing.T) {
err := Twilio.OnSuccess(exampleService)
_, err := Twilio.OnSuccess(exampleService)
assert.Nil(t, err)
})

View File

@ -32,6 +32,9 @@ var Webhook = &webhooker{&notifications.Notification{
AuthorUrl: "https://github.com/hunterlong",
Icon: "fas fa-code-branch",
Delay: time.Duration(1 * time.Second),
SuccessData: `{"id": {{.Service.Id}}, "online": true}`,
FailureData: `{"id": {{.Service.Id}}, "online": false}`,
DataType: "json",
Limits: 180,
Form: []notifications.NotificationForm{{
Type: "text",
@ -47,12 +50,6 @@ var Webhook = &webhooker{&notifications.Notification{
SmallText: "Choose a HTTP method for example: GET, POST, DELETE, or PATCH.",
DbField: "Var1",
Required: true,
}, {
Type: "textarea",
Title: "HTTP Body",
Placeholder: `{"service_id": {{.Service.Id}}", "service_name": "{{.Service.Name}"}`,
SmallText: "Optional HTTP body for a POST request. You can insert variables into your body request.<br>{{.Service.Id}}, {{.Service.Name}}, {{.Service.Online}}<br>{{.Failure.Issue}}",
DbField: "Var2",
}, {
Type: "text",
Title: "Content Type",
@ -114,7 +111,9 @@ func (w *webhooker) sendHttpWebhook(body string) (*http.Response, error) {
}
func (w *webhooker) OnTest() (string, error) {
body := ReplaceVars(w.Var2, exampleService, exampleFailure)
f := failures.Example()
s := services.Example(false)
body := ReplaceVars(w.SuccessData, s, f)
resp, err := w.sendHttpWebhook(body)
if err != nil {
return "", err
@ -127,23 +126,25 @@ func (w *webhooker) OnTest() (string, error) {
}
// OnFailure will trigger failing service
func (w *webhooker) OnFailure(s *services.Service, f *failures.Failure) error {
msg := ReplaceVars(w.Var2, s, f)
func (w *webhooker) OnFailure(s *services.Service, f *failures.Failure) (string, error) {
msg := ReplaceVars(w.FailureData, s, f)
resp, err := w.sendHttpWebhook(msg)
if err != nil {
return err
return "", err
}
defer resp.Body.Close()
return err
content, err := ioutil.ReadAll(resp.Body)
return string(content), err
}
// OnSuccess will trigger successful service
func (w *webhooker) OnSuccess(s *services.Service) error {
msg := ReplaceVars(w.Var2, s, nil)
func (w *webhooker) OnSuccess(s *services.Service) (string, error) {
msg := ReplaceVars(w.SuccessData, s, nil)
resp, err := w.sendHttpWebhook(msg)
if err != nil {
return err
return "", err
}
defer resp.Body.Close()
return err
content, err := ioutil.ReadAll(resp.Body)
return string(content), err
}

View File

@ -45,12 +45,12 @@ func TestWebhookNotifier(t *testing.T) {
})
t.Run("webhooker OnFailure", func(t *testing.T) {
err := Webhook.OnFailure(exampleService, exampleFailure)
_, err := Webhook.OnFailure(exampleService, exampleFailure)
assert.Nil(t, err)
})
t.Run("webhooker OnSuccess", func(t *testing.T) {
err := Webhook.OnSuccess(exampleService)
_, err := Webhook.OnSuccess(exampleService)
assert.Nil(t, err)
})

View File

@ -6,7 +6,6 @@ import (
"fmt"
"github.com/GeertJohan/go.rice"
"github.com/pkg/errors"
"github.com/russross/blackfriday/v2"
"github.com/statping/statping/utils"
"os"
"os/exec"
@ -30,12 +29,6 @@ func Assets() error {
return err
}
// HelpMarkdown will return the Markdown of help.md into HTML
func HelpMarkdown() string {
output := blackfriday.Run(CompiledWiki)
return string(output)
}
func scssRendered(name string) string {
spl := strings.Split(name, "/")
path := spl[:len(spl)-2]
@ -137,6 +130,9 @@ func CreateAllAssets(folder string) error {
if err := MakePublicFolder(fp(folder, "assets", "css")); err != nil {
return err
}
if err := MakePublicFolder(fp(folder, "assets", "favicon")); err != nil {
return err
}
if err := MakePublicFolder(fp(folder, "assets", "scss")); err != nil {
return err
}

View File

@ -4,6 +4,7 @@ import (
"github.com/pkg/errors"
"github.com/statping/statping/utils"
"net/http"
"strconv"
)
func LoadConfigForm(r *http.Request) (*DbConfig, error) {
@ -24,6 +25,7 @@ func LoadConfigForm(r *http.Request) (*DbConfig, error) {
domain := g("domain")
email := g("email")
language := g("language")
reports, _ := strconv.ParseBool(g("send_reports"))
if project == "" || username == "" || password == "" {
err := errors.New("Missing required elements on setup form")
@ -40,6 +42,7 @@ func LoadConfigForm(r *http.Request) (*DbConfig, error) {
p.Set("NAME", project)
p.Set("DESCRIPTION", description)
p.Set("LANGUAGE", language)
p.Set("ALLOW_REPORTS", reports)
confg := &DbConfig{
DbConn: dbConn,
@ -56,8 +59,8 @@ func LoadConfigForm(r *http.Request) (*DbConfig, error) {
Email: email,
Location: utils.Directory,
Language: language,
SendReports: reports,
}
return confg, nil
}

View File

@ -49,6 +49,12 @@ func LoadConfigFile(configFile string) (*DbConfig, error) {
if db.ApiSecret != "" {
p.Set("API_SECRET", db.ApiSecret)
}
if db.Language != "" {
p.Set("LANGUAGE", "en")
}
if db.SendReports {
p.Set("ALLOW_REPORTS", true)
}
configs := &DbConfig{
DbConn: p.GetString("DB_CONN"),
@ -65,6 +71,8 @@ func LoadConfigFile(configFile string) (*DbConfig, error) {
Password: p.GetString("ADMIN_PASSWORD"),
Location: utils.Directory,
SqlFile: p.GetString("SQL_FILE"),
Language: p.GetString("LANGUAGE"),
SendReports: p.GetBool("ALLOW_REPORTS"),
}
log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + configFile)

View File

@ -14,6 +14,7 @@ type DbConfig struct {
DbPort int `yaml:"port" json:"-"`
ApiSecret string `yaml:"api_secret" json:"-"`
Language string `yaml:"language" json:"language"`
SendReports bool `yaml:"send_reports" json:"send_reports"`
Project string `yaml:"-" json:"-"`
Description string `yaml:"-" json:"-"`
Domain string `yaml:"-" json:"-"`

View File

@ -21,6 +21,7 @@ func Samples() error {
UseCdn: null.NewNullBool(false),
Footer: null.NewNullString(""),
MigrationId: utils.Now().Unix(),
Language: utils.Params.GetString("LANGUAGE"),
}
return core.Create()

View File

@ -12,6 +12,20 @@ var (
log = utils.Log.WithField("type", "failure")
)
func Example() *Failure {
return &Failure{
Id: 48533,
Issue: "Response did not response a 200 status code",
Method: "",
MethodId: 0,
ErrorCode: 404,
Service: 1,
Checkin: 0,
PingTime: 48309,
CreatedAt: utils.Now(),
}
}
func Samples() error {
log.Infoln("Inserting Sample Service Failures...")
createdAt := utils.Now().Add(-3 * types.Day)

View File

@ -6,38 +6,6 @@ import (
)
var (
serviceOnline = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "service_online",
Help: "How many failures occur for a service",
},
[]string{"service"},
)
serviceFailures = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "service_failures",
Help: "How many failures occur for a service",
},
[]string{"service"},
)
serviceSuccess = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "service_success",
Help: "How many successful requests for a service",
},
[]string{"service"},
)
serviceLatencyDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "service_request_duration",
Help: "How many successful requests for a service",
},
[]string{"service"},
)
utilsHttpRequestDur = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_requests_duration",
@ -60,6 +28,7 @@ func init() {
serviceOnline,
serviceFailures,
serviceSuccess,
serviceStatusCode,
serviceLatencyDuration,
utilsHttpRequestDur,
utilsHttpRequestBytes,
@ -77,6 +46,15 @@ func Histo(method string, value float64, labels ...interface{}) {
}
}
func Gauge(method string, value float64, labels ...interface{}) {
switch method {
case "service":
serviceStatusCode.WithLabelValues(convert(labels)...).Set(value)
case "online":
serviceOnline.WithLabelValues(convert(labels)...).Set(value)
}
}
func Inc(method string, labels ...interface{}) {
switch method {
case "failure":

50
types/metrics/services.go Normal file
View File

@ -0,0 +1,50 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
// service is online if set to 1, offline if 0
serviceOnline = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "service_online",
Help: "If service is online",
},
[]string{"service"},
)
// service failures
serviceFailures = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "service_failures",
Help: "How many failures occur for a service",
},
[]string{"service"},
)
// successful hits for a service
serviceSuccess = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "service_success",
Help: "How many successful requests for a service",
},
[]string{"service"},
)
// service check latency
serviceLatencyDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "service_latency",
Help: "How many successful requests for a service",
},
[]string{"service"},
)
// http status code for a service
serviceStatusCode = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "service_status_code",
Help: "HTTP Status code for a service",
},
[]string{"service"},
)
)

View File

@ -24,11 +24,22 @@ func Find(method string) (*Notification, error) {
}
func (n *Notification) Create() error {
q := db.Where("method = ?", n.Method).Find(n)
var p Notification
q := db.Where("method = ?", n.Method).Find(&p)
if q.RecordNotFound() {
if err := db.Create(n).Error(); err != nil {
return err
}
return nil
}
if p.FailureData == "" {
p.FailureData = n.FailureData
}
if p.SuccessData == "" {
p.SuccessData = n.SuccessData
}
if err := p.Update(); err != nil {
return err
}
return nil
}
@ -44,6 +55,8 @@ func (n *Notification) UpdateFields(notif *Notification) *Notification {
n.ApiSecret = notif.ApiSecret
n.Var1 = notif.Var1
n.Var2 = notif.Var2
n.SuccessData = notif.SuccessData
n.FailureData = notif.FailureData
return n
}

View File

@ -26,6 +26,10 @@ type Notification struct {
Enabled null.NullBool `gorm:"column:enabled;type:boolean;default:false" json:"enabled,omitempty"`
Limits int `gorm:"not null;column:limits" json:"limits"`
Removable bool `gorm:"column:removable" json:"removable"`
SuccessData string `gorm:"not null;column:success_data" json:"success_data,omitempty"`
FailureData string `gorm:"not null;column:failure_data" json:"failure_data,omitempty"`
DataType string `gorm:"-" json:"data_type,omitempty"`
RequestInfo string `gorm:"-" json:"request_info,omitempty"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Title string `gorm:"-" json:"title"`

View File

@ -7,7 +7,7 @@ import (
// Notifier interface is required to create a new Notifier
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() (string, error) // OnTest is triggered for testing
OnSuccess(*services.Service) (string, error) // OnSuccess is triggered when a service is successful
OnFailure(*services.Service, *failures.Failure) (string, error) // OnFailure is triggered when a service is failing
OnTest() (string, error) // OnTest is triggered for testing
}

View File

@ -26,8 +26,8 @@ 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() (string, error) // OnTest is triggered for testing
Select() *notifications.Notification // OnTest is triggered for testing
OnSuccess(*Service) (string, error) // OnSuccess is triggered when a service is successful
OnFailure(*Service, *failures.Failure) (string, error) // OnFailure is triggered when a service is failing
OnTest() (string, error) // OnTest is triggered for testing
Select() *notifications.Notification // OnTest is triggered for testing
}

View File

@ -276,6 +276,7 @@ func CheckHttp(s *Service, record bool) (*Service, error) {
metrics.Histo("latency", utils.Now().Sub(t1).Seconds(), s.Id)
s.LastResponse = string(content)
s.LastStatusCode = res.StatusCode
metrics.Gauge("service", float64(res.StatusCode), s.Id)
if s.Expected.String != "" {
match, err := regexp.MatchString(s.Expected.String, string(content))
@ -321,6 +322,7 @@ func recordSuccess(s *Service) {
s.LastLatency = hit.Latency
sendSuccess(s)
s.SuccessNotified = true
metrics.Gauge("online", 1., s.Id)
}
func AddNotifier(n ServiceNotifier) {
@ -341,7 +343,7 @@ func sendSuccess(s *Service) {
notif := n.Select()
if notif.CanSend() {
log.Infof("Sending notification to: %s!", notif.Method)
if err := n.OnSuccess(s); err != nil {
if _, err := n.OnSuccess(s); err != nil {
notif.Logger().Errorln(err)
}
s.UserNotified = true
@ -373,6 +375,7 @@ func recordFailure(s *Service, issue string) {
s.SuccessNotified = false
s.DownText = s.DowntimeText()
sendFailure(s, fail)
metrics.Gauge("online", 0., s.Id)
}
func sendFailure(s *Service, f *failures.Failure) {
@ -391,7 +394,7 @@ func sendFailure(s *Service, f *failures.Failure) {
notif := n.Select()
if notif.CanSend() {
log.Infof("Sending Failure notification to: %s!", notif.Method)
if err := n.OnFailure(s, f); err != nil {
if _, err := n.OnFailure(s, f); err != nil {
notif.Logger().WithField("failure", f.Issue).Errorln(err)
}
s.UserNotified = true

View File

@ -6,6 +6,61 @@ import (
"time"
)
func Example(online bool) *Service {
return &Service{
Id: 6283,
Name: "Statping Example",
Domain: "https://localhost:8080",
Expected: null.NewNullString(""),
ExpectedStatus: 200,
Interval: int(time.Duration(15 * time.Second).Seconds()),
Type: "http",
Method: "get",
PostData: null.NullString{},
Port: 0,
Timeout: int(time.Duration(2 * time.Second).Seconds()),
Order: 0,
VerifySSL: null.NewNullBool(true),
Public: null.NewNullBool(true),
GroupId: 0,
TLSCert: null.NullString{},
TLSCertKey: null.NullString{},
TLSCertRoot: null.NullString{},
Headers: null.NullString{},
Permalink: null.NewNullString("example-service"),
Redirect: null.NewNullBool(true),
CreatedAt: utils.Now().Add(-23 * time.Hour),
UpdatedAt: utils.Now().Add(-23 * time.Hour),
Online: online,
Latency: 393443,
PingTime: 83526,
Online24Hours: 0.98,
Online7Days: 0.99,
AvgResponse: 303443,
FailuresLast24Hours: 2,
Checkpoint: time.Time{},
SleepDuration: 5 * time.Second,
LastResponse: "The example service is hitting this page",
NotifyAfter: 0,
notifyAfterCount: 0,
AllowNotifications: null.NewNullBool(true),
UserNotified: false,
UpdateNotify: null.NewNullBool(true),
DownText: "The service ws responding with 500 status code",
SuccessNotified: false,
LastStatusCode: 200,
Failures: nil,
AllCheckins: nil,
LastLookupTime: 4600,
LastLatency: 124399,
LastCheck: utils.Now().Add(-37 * time.Second),
LastOnline: utils.Now().Add(-37 * time.Second),
LastOffline: utils.Now().Add((-14 * 24) * time.Hour),
SecondsOnline: int64(utils.Now().Add(-24 * time.Hour).Second()),
SecondsOffline: int64(utils.Now().Add(-150 * time.Second).Second()),
}
}
func Samples() error {
log.Infoln("Inserting Sample Services...")
createdOn := utils.Now().Add(((-24 * 30) * 3) * time.Hour)

View File

@ -1 +1 @@
0.90.50
0.90.51