custom notifier JSON/text, codemirror custom designs for variables, JWT updates

pull/663/head
hunterlong 2020-06-14 21:46:28 -07:00
parent 722ce47977
commit 01fea69edc
32 changed files with 433 additions and 177 deletions

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

@ -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

@ -121,20 +121,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,22 +9,22 @@
<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">

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,13 +26,13 @@
</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>
<label>{{ $t('setup.language') }}</label>
<select v-model="core.language" class="form-control">
<option value="en">English</option>
<option value="es">Spanish</option>
@ -40,11 +40,10 @@
<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}}
@ -10,8 +11,6 @@
</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 +29,72 @@
</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}}<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>
</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" 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" 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,16 +109,31 @@
</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,
@ -87,10 +142,55 @@ export default {
response: 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)
console.log('the editor is ready!', cm)
setTimeout(function() {
cm.refresh();
},1);
},
onCmFailureReady(cm) {
this.failure_data = beautify(this.notifier.failure_data, this.beautifySettings)
setTimeout(function() {
cm.refresh();
},1);
},
onCmFocus(cm) {
console.log('the editor is focused!', cm)
},
onCmCodeChange(newCode) {
console.log('this is new code', newCode)
this.success_data = newCode
},
async enableToggle() {
this.notifier.enabled = !!this.notifier.enabled
const form = {
@ -112,6 +212,9 @@ export default {
}
this.form[field] = val
});
this.form.success_data = this.success_data
this.form.failure_data = this.failure_data
window.console.log(this.form)
await Api.notifier_save(this.form)
const notifiers = await Api.notifiers()
await this.$store.commit('setNotifiers', notifiers)
@ -142,7 +245,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,12 @@
const english = {
top_nav: {
dashboard: "Dashboard",
services: "Services",
users: "Users",
announcements: "Announcements",
settings: "Settings",
logs: "Logs",
},
setup: {
language: "Language",
connection: "Database Connection",
@ -9,19 +17,41 @@ 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',
offline: 'offline',
incident: 'incident',
group: 'group',
group: 'Group',
message: 'message',
logout: 'logout',
sample_data: 'Sample Data',

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

@ -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

@ -45,6 +45,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)

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.",
}}},
}
@ -59,16 +50,14 @@ 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)
tmpl := ReplaceVars(c.SuccessData, s, nil)
_, _, err := runCommand(c.Host, tmpl)
return 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)
tmpl := ReplaceVars(c.FailureData, s, f)
_, _, err := runCommand(c.Host, tmpl)
return err
}

View File

@ -19,6 +19,11 @@ type discord struct {
*notifications.Notification
}
var (
successData = `{"content": "Your service '{{.Service.Name}}' is currently online!"}`
failureData = `{"content": "Your service '{{.Service.Name}}' is currently failing! Reason: {{.Failure.Issue}}"}`
)
var Discorder = &discord{&notifications.Notification{
Method: "discord",
Title: "discord",
@ -28,6 +33,9 @@ var Discorder = &discord{&notifications.Notification{
Delay: time.Duration(5 * time.Second),
Host: "https://discordapp.com/api/webhooks/****/*****",
Icon: "fab fa-discord",
SuccessData: successData,
FailureData: failureData,
DataType: "json",
Limits: 60,
Form: []notifications.NotificationForm{{
Type: "text",

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",
@ -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

@ -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",
@ -68,15 +71,15 @@ 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)
message := ReplaceVars(t.FailureData, s, f)
_, err := t.sendMessage(message)
return 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)
message := ReplaceVars(t.SuccessData, s, nil)
_, err := t.sendMessage(message)
return 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",

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",
@ -72,14 +75,14 @@ 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)
msg := ReplaceVars(t.FailureData, s, f)
_, 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)
msg := ReplaceVars(t.SuccessData, s, nil)
_, err := t.sendMessage(msg)
return 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",
@ -88,14 +91,14 @@ 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)
msg := ReplaceVars(t.FailureData, s, f)
_, 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)
msg := ReplaceVars(t.SuccessData, s, nil)
_, err := t.sendMessage(msg)
return 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,7 @@ func (w *webhooker) sendHttpWebhook(body string) (*http.Response, error) {
}
func (w *webhooker) OnTest() (string, error) {
body := ReplaceVars(w.Var2, exampleService, exampleFailure)
body := ReplaceVars(w.SuccessData, exampleService, exampleFailure)
resp, err := w.sendHttpWebhook(body)
if err != nil {
return "", err
@ -128,7 +125,7 @@ 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)
msg := ReplaceVars(w.FailureData, s, f)
resp, err := w.sendHttpWebhook(msg)
if err != nil {
return err
@ -139,7 +136,7 @@ func (w *webhooker) OnFailure(s *services.Service, f *failures.Failure) error {
// OnSuccess will trigger successful service
func (w *webhooker) OnSuccess(s *services.Service) error {
msg := ReplaceVars(w.Var2, s, nil)
msg := ReplaceVars(w.SuccessData, s, nil)
resp, err := w.sendHttpWebhook(msg)
if 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

@ -24,12 +24,20 @@ 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
}
}
if p.FailureData == "" || p.SuccessData == "" {
p.SuccessData = n.SuccessData
p.FailureData = n.FailureData
if err := p.Update(); err != nil {
return err
}
}
return nil
}
@ -44,6 +52,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"`