* notifier panic fix

* portainer template

* remove default host from discord notifier

* fix for updating fields

* fix for updating fields

* fixed notifier panic

* fixed notifier panic

* test fix

* test fix

* missing login banner image

* dont delete admin if DEMO_MODE

* updatess to service on Dashboard

* notifier endpoint fixes, timeframe rounding chart data

* modal for UI confirmations
pull/815/head^2
Hunter Long 2020-09-02 14:00:31 -07:00 committed by GitHub
parent 243b6f019f
commit 5d85c3ce39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 449 additions and 191 deletions

View File

@ -257,6 +257,7 @@ jobs:
API_SECRET: demopassword123
DISABLE_LOGS: false
ALLOW_REPORTS: true
SAMPLE_DATA: true
COVERALLS: ${{ secrets.COVERALLS }}
DISCORD_URL: ${{ secrets.DISCORD_URL }}
EMAIL_HOST: ${{ secrets.EMAIL_HOST }}

View File

@ -257,6 +257,7 @@ jobs:
API_SECRET: demopassword123
DISABLE_LOGS: false
ALLOW_REPORTS: true
SAMPLE_DATA: true
COVERALLS: ${{ secrets.COVERALLS }}
DISCORD_URL: ${{ secrets.DISCORD_URL }}
EMAIL_HOST: ${{ secrets.EMAIL_HOST }}

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ snap
prime
stage
parts
assets_backup
certs
releases
core/rice-box.go

View File

@ -1,3 +1,10 @@
# 0.90.65 (09-01-2020)
- Fixed issue with dashboard not logging in (notifier panic)
- Modified static email templates to github.com/statping/emails
- Modified Regenerate API function to keep API_SECRET env
- Added DEMO_MODE env variable, if true, 'admin' cannot be deleted
- Modified Service sparklines on Dashboard
# 0.90.64 (08-18-2020)
- Modified max-width for container to 1012px, larger UI
- Added failure sparklines in the Services list view

View File

@ -148,20 +148,20 @@ func InitApp() error {
if _, err := core.Select(); err != nil {
return err
}
// init Sentry error monitoring (its useful)
utils.SentryInit(core.App.AllowReports.Bool)
// init prometheus metrics
metrics.InitMetrics()
// connect each notifier, added them into database if needed
notifiers.InitNotifiers()
// select all services in database and store services in a mapping of Service pointers
if _, err := services.SelectAllServices(true); err != nil {
return err
}
// start routines for each service checking process
services.CheckServices()
// connect each notifier, added them into database if needed
notifiers.InitNotifiers()
// start routine to delete old records (failures, hits)
go database.Maintenance()
// init Sentry error monitoring (its useful)
utils.SentryInit(core.App.AllowReports.Bool)
core.App.Setup = true
core.App.Started = utils.Now()
return nil

View File

@ -70,19 +70,19 @@ func (t *TimeVar) ToValues() ([]*TimeValue, error) {
}
// GraphData will return all hits or failures
func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
g.db = g.db.MultipleSelects(
g.db.SelectByTime(g.Group),
func (b *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
b.db = b.db.MultipleSelects(
b.db.SelectByTime(b.Group),
by.String(),
).Group("timeframe").Order("timeframe", true)
caller, err := g.ToTimeValue()
caller, err := b.ToTimeValue()
if err != nil {
return nil, err
}
if g.FillEmpty {
return caller.FillMissing(g.Start, g.End)
if b.FillEmpty {
return caller.FillMissing(b.Start, b.End)
}
return caller.ToValues()
}
@ -90,8 +90,8 @@ func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
// ToTimeValue will format the SQL rows into a JSON format for the API.
// [{"timestamp": "2006-01-02T15:04:05Z", "amount": 468293}]
// TODO redo this entire function, use better SQL query to group by time
func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
rows, err := g.db.Rows()
func (b *GroupQuery) ToTimeValue() (*TimeVar, error) {
rows, err := b.db.Rows()
if err != nil {
return nil, err
}
@ -102,8 +102,8 @@ func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
if err := rows.Scan(&timeframe, &amount); err != nil {
log.Error(err, timeframe)
}
trueTime, _ := g.db.ParseTime(timeframe)
newTs := types.FixedTime(trueTime, g.Group)
trueTime, _ := b.db.ParseTime(timeframe)
newTs := types.FixedTime(trueTime, b.Group)
tv := &TimeValue{
Timeframe: newTs,
@ -111,7 +111,7 @@ func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
}
data = append(data, tv)
}
return &TimeVar{g, data}, nil
return &TimeVar{b, data}, nil
}
func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) {

21
dev/portainer.json vendored Normal file
View File

@ -0,0 +1,21 @@
[
{
"type": 1,
"title": "Statping",
"restart_policy": "unless-stopped",
"description": "Service monitoring with an easy to use status page and mobile app",
"logo": "https://assets.statping.com/icon.png",
"image": "statping/statping:latest",
"platform": "linux",
"categories": ["monitoring"],
"administrator_only": false,
"ports": [
"8080:8080/tcp"
],
"volumes": [
{
"container": "/app"
}
]
}
]

View File

@ -74,7 +74,7 @@ const webpackConfig = merge(commonConfig, {
threshold: 10240,
minRatio: 0.8
}),
new webpack.HashedModuleIdsPlugin(),
// new webpack.HashedModuleIdsPlugin(),
new HtmlPlugin({
template: 'public/base.gohtml',
filename: 'base.gohtml',

View File

@ -7,8 +7,8 @@ const tokenKey = "statping_auth";
class Api {
constructor() {
this.version = "0.90.64";
this.commit = "130cc3ede7463ec9af8d62abb84992e2a0ef453c";
this.version = "0.90.65";
this.commit = "5bc10fcc8536a08ce7a099a0b4cbceb2dc9fc35b";
}
async oauth() {

View File

@ -85,13 +85,13 @@
.chartmarker {
padding: 0px;
width: 200px;
text-align: right;
text-align: left;
}
.chartmarker SPAN {
font-size: 4pt;
display: block;
color: #8b8b8b;
color: #b1b1b1;
}
.apexcharts-tooltip {

View File

@ -14,6 +14,34 @@ A:HOVER {
color: lighten($text-color, 12%) !important;
}
.modal-backdrop {
top: 0;
left: 0;
position: absolute;
display: block;
z-index: 10000;
width: 100%;
height: 100%;
background-color: #00000073;
}
.modal {
z-index: 999999 !important;
display: block;
}
.modal-dialog {
top: 20%;
}
.modal-header {
padding: 0.5rem 1rem;
}
.modal-footer {
padding: 0.5rem 1rem;
}
.text-muted {
color: lighten($text-color, 30%) !important;
}

View File

@ -81,13 +81,21 @@
serviceName (service) {
return service.name || "Global Message"
},
async delete(m) {
await Api.message_delete(m.id)
const messages = await Api.messages()
this.$store.commit('setMessages', messages)
},
async deleteMessage(m) {
let c = confirm(`Are you sure you want to delete message '${m.title}'?`)
if (c) {
await Api.message_delete(m.id)
const messages = await Api.messages()
this.$store.commit('setMessages', messages)
const modal = {
visible: true,
title: "Delete Announcement",
body: `Are you sure you want to delete Announcement ${m.title}?`,
btnColor: "btn-danger",
btnText: "Delete Announcement",
func: () => this.delete(m),
}
this.$store.commit("setModal", modal)
}
}
}

View File

@ -1,5 +1,6 @@
<template>
<div class="col-12">
<div class="card contain-card mb-4">
<div class="card-header">{{ $t('top_nav.services') }}
<router-link v-if="$store.state.admin" to="/dashboard/create_service" class="btn btn-sm btn-success float-right">
@ -67,6 +68,7 @@
</template>
<script>
const Modal = () => import(/* webpackChunkName: "dashboard" */ "@/components/Elements/Modal")
const FormGroup = () => import(/* webpackChunkName: "dashboard" */ '@/forms/Group')
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '@/forms/ToggleSwitch')
const ServicesList = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServicesList')
@ -76,6 +78,7 @@
export default {
name: 'DashboardServices',
components: {
Modal,
ServicesList,
ToggleSwitch,
FormGroup,
@ -112,13 +115,24 @@
this.group = g
this.edit = !mode
},
confirm_delete(service) {
},
async delete(g) {
await Api.group_delete(g.id)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
},
async deleteGroup(g) {
let c = confirm(`Are you sure you want to delete '${g.name}'?`)
if (c) {
await Api.group_delete(g.id)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
}
const modal = {
visible: true,
title: "Delete Group",
body: `Are you sure you want to delete group ${g.name}? All services attached will be removed from this group.`,
btnColor: "btn-danger",
btnText: "Delete Group",
func: () => this.delete(g),
}
this.$store.commit("setModal", modal)
}
}
}

View File

@ -74,13 +74,21 @@
this.user = u
this.edit = !mode
},
async delete(u) {
await Api.user_delete(u.id)
const users = await Api.users()
this.$store.commit('setUsers', users)
},
async deleteUser(u) {
let c = confirm(`Are you sure you want to delete user '${u.username}'?`)
if (c) {
await Api.user_delete(u.id)
const users = await Api.users()
this.$store.commit('setUsers', users)
const modal = {
visible: true,
title: "Delete User",
body: `Are you sure you want to delete user ${u.username}?`,
btnColor: "btn-danger",
btnText: "Delete User",
func: () => this.delete(u),
}
this.$store.commit("setModal", modal)
}
}
}

View File

@ -151,14 +151,22 @@ export default {
await this.gotoPage(1)
},
methods: {
async delete() {
await Api.service_failures_delete(this.service)
this.service = await Api.service(this.service.id)
this.total = 0
await this.load()
},
async deleteFailures() {
const c = confirm('Are you sure you want to delete all failures?')
if (c) {
await Api.service_failures_delete(this.service)
this.service = await Api.service(this.service.id)
this.total = 0
await this.load()
const modal = {
visible: true,
title: "Delete All Failures",
body: `Are you sure you want to delete all Failures for service ${this.service.title}?`,
btnColor: "btn-danger",
btnText: "Delete Failures",
func: () => this.delete(),
}
this.$store.commit("setModal", modal)
},
async gotoPage(page) {
this.page = page;

View File

@ -1,6 +1,6 @@
<template>
<div class="row">
<h5 v-if="group.name" class="h5 col-12 mb-3 mt-2 text-dim">
<h5 v-if="group.name && group_services" class="h5 col-12 mb-3 mt-2 text-dim">
<font-awesome-icon @click="toggle" :icon="expanded ? 'minus' : 'plus'" class="pointer mr-3"/> {{group.name}}
<span class="badge badge-success text-uppercase float-right ml-2">{{services_online.length}} online</span>
<span v-if="services_online.services_offline > 0" class="badge badge-danger text-uppercase float-right">

View File

@ -80,15 +80,23 @@ const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/
methods: {
async delete(i) {
this.res = await Api.incident_delete(i)
if (this.res.status === "success") {
this.incidents = this.incidents.filter(obj => obj.id !== i.id);
//await this.loadIncidents()
}
},
async deleteIncident(incident) {
let c = confirm(`Are you sure you want to delete '${incident.title}'?`)
if (c) {
this.res = await Api.incident_delete(incident)
if (this.res.status === "success") {
this.incidents = this.incidents.filter(obj => obj.id !== incident.id); // this is better in terms of not having to querry the db to get a fresh copy of all updates
//await this.loadIncidents()
} // TODO: further error checking here... maybe alert user it failed with modal or so
}
const modal = {
visible: true,
title: "Delete Incident",
body: `Are you sure you want to delete Incident ${incident.title}?`,
btnColor: "btn-danger",
btnText: "Delete Incident",
func: () => this.delete(incident),
}
this.$store.commit("setModal", modal)
},
async createIncident() {

View File

@ -32,7 +32,7 @@
<div class="row">
<div class="col-5 pr-0">
<span class="small text-dim"> {{ hoverbtn }}</span>
<span class="small text-dim">{{ hoverbtn }}</span>
</div>
<div class="col-7 pr-2 pl-0">
@ -121,13 +121,14 @@
}
},
async getUptime() {
const start = this.nowSubtract(3 * 86400)
this.uptime = await Api.service_uptime(this.service.id, this.toUnix(start), this.toUnix(this.now()))
const end = this.endOf("day", this.now())
const start = this.beginningOf("day", this.nowSubtract(3 * 86400))
this.uptime = await Api.service_uptime(this.service.id, this.toUnix(start), this.toUnix(end))
},
async loadInfo() {
this.set1 = await this.getHits(24 * 7, "6h")
this.set1 = await this.getHits(86400 * 7, "12h")
this.set1_name = this.calc(this.set1)
this.set2 = await this.getHits(24, "1h")
this.set2 = await this.getHits(86400, "60m")
this.set2_name = this.calc(this.set2)
this.loaded = true
},
@ -145,14 +146,13 @@
});
total = total / data.length
},
async getHits(hours, group) {
const start = this.nowSubtract(3600 * hours)
const fetched = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(this.now()), group, false)
async getHits(seconds, group) {
let start = this.nowSubtract(seconds)
let end = this.endOf("today")
const startEnd = this.startEndParams(start, end, group)
const fetched = await Api.service_hits(this.service.id, startEnd.start, startEnd.end, group, true)
const data = this.convertToChartData(fetched, 0.001, true)
return [{name: "Latency", ...data}]
},
calc(s) {
let data = s[0].data

View File

@ -57,13 +57,13 @@
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[seriesIndex][dataPointIndex];
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${this.humanTime(val)}</span><span>${dt}</span></div>`
return `<div class="chartmarker"><span class="font-3">Average Response Time: ${this.humanTime(val)}</span><span>${dt}</span></div>`
},
fixed: {
enabled: true,
position: 'topRight',
position: 'bottomLeft',
offsetX: 0,
offsetY: 0,
offsetY: -30,
},
x: {
show: true,
@ -94,7 +94,3 @@
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -72,12 +72,14 @@
<script>
import Api from "../../API";
import ServiceSparkList from "@/components/Service/ServiceSparkList";
import Modal from "@/components/Elements/Modal";
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable')
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '../../forms/ToggleSwitch');
export default {
name: 'ServicesList',
components: {
Modal,
ServiceSparkList,
ToggleSwitch,
draggable
@ -159,14 +161,25 @@ export default {
await Api.services_reorder(data)
await this.update()
},
tester(s) {
console.log(s)
},
async delete(s) {
this.loading = true
await Api.service_delete(s.id)
await this.update()
this.loading = false
},
async deleteService(s) {
let c = confirm(`Are you sure you want to delete '${s.name}'?`)
if (c) {
this.loading = true
await Api.service_delete(s.id)
await this.update()
this.loading = false
}
const modal = {
visible: true,
title: "Delete Service",
body: `Are you sure you want to delete service ${s.name}? This will also delete all failures, checkins, and incidents for this service.`,
btnColor: "btn-danger",
btnText: "Delete Service",
func: () => this.delete(s),
}
this.$store.commit("setModal", modal)
},
serviceGroup(s) {
let group = this.$store.getters.groupById(s.group_id)

View File

@ -144,14 +144,22 @@ import('codemirror/mode/css/css.js')
this.pending = false
await this.fetchTheme()
},
async delete() {
this.pending = true
const resp = await Api.theme_generate(false)
await this.fetchTheme()
this.pending = false
},
async deleteAssets() {
this.pending = true
let c = confirm('Are you sure you want to delete all local assets?')
if (c) {
const resp = await Api.theme_generate(false)
await this.fetchTheme()
}
this.pending = false
const modal = {
visible: true,
title: "Delete Local Assets",
body: `Are you sure you want to delete all local assets?`,
btnColor: "btn-danger",
btnText: "Delete",
func: () => this.delete(),
}
this.$store.commit("setModal", modal)
},
async saveAssets() {
this.pending = true

View File

@ -0,0 +1,50 @@
<template>
<div v-if="modal.visible" class="modal d-block" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{modal.title}}</h5>
</div>
<div class="modal-body">
<p>{{modal.body}}</p>
</div>
<div class="modal-footer">
<button @click.prevent="close" type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button @click.prevent="runFunc" type="button" :class="`btn ${modal.btnColor}`">{{modal.btnText}}</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Modal",
data () {
return {
}
},
computed: {
modal() {
return this.$store.getters.modal
}
},
mounted() {
},
methods: {
runFunc() {
this.$store.getters.modal.func()
this.close()
},
close() {
this.$store.commit("setModal", {visible: false})
}
}
}
</script>
<style scoped>
</style>

View File

@ -157,7 +157,8 @@ export default {
},
async loadFailures() {
this.loaded = false
const data = await Api.service_failures_data(this.service.id, this.toUnix(this.parseISO(this.start)), this.toUnix(this.parseISO(this.end)), this.group, true)
const startEnd = this.startEndParams(this.parseISO(this.start), this.parseISO(this.end), this.group)
const data = await Api.service_failures_data(this.service.id, startEnd.start, startEnd.end, this.group, true)
this.loaded = true
this.data = [{data: this.convertChartData(data)}]
}

View File

@ -61,7 +61,6 @@
const Analytics = () => import(/* webpackChunkName: "service" */ './Analytics');
const ServiceChart = () => import(/* webpackChunkName: "service" */ "./ServiceChart");
const ServiceTopStats = () => import(/* webpackChunkName: "service" */ "@/components/Service/ServiceTopStats");
const Graphing = () => import(/* webpackChunkName: "service" */ '../../graphing');
export default {
name: 'ServiceBlock',

View File

@ -195,17 +195,14 @@
methods: {
async chartHits(val) {
this.ready = false
const start = val.start_time
const end = this.toUnix(new Date())
this.data = await Api.service_hits(this.service.id, start, end, val.interval, false)
if (this.data === null && val.interval !== "5m") {
await this.chartHits({start_time: val.start_time, interval: "5m"})
}
this.ping_data = await Api.service_ping(this.service.id, start, end, val.interval, false)
const end = this.endOf("hour", this.now())
const start = this.beginningOf("hour", this.fromUnix(val.start_time))
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(end), val.interval, false)
this.ping_data = await Api.service_ping(this.service.id, this.toUnix(start), this.toUnix(end), val.interval, false)
this.series = [
{name: "Latency", ...this.convertToChartData(this.data)},
{name: "Ping", ...this.convertToChartData(this.ping_data)},
{name: "Ping", ...this.convertToChartData(this.ping_data)},
]
this.ready = true
}

View File

@ -10,7 +10,7 @@
<div class="form-group row">
<label for="password" class="col-4 col-form-label">{{$t('password')}}</label>
<div class="col-8">
<input @keyup="checkForm" type="password" v-model="password" autocomplete="current-password" name="password" class="form-control" id="password" placeholder="password123">
<input @keyup="checkForm" type="password" v-model="password" autocomplete="current-password" name="password" class="form-control" id="password" placeholder="************">
</div>
</div>
<div class="form-group row">

View File

@ -149,11 +149,11 @@
<div v-for="(log, i) in notifier.logs.reverse()" class="alert" :class="{'alert-danger': log.error, 'alert-dark': !log.success && !log.error, 'alert-success': log.success && !log.error}">
<span class="d-block">
Service '{{$store.getters.serviceById(log.service).name}}'
Service {{log.service}}
{{log.success ? "Success Triggered" : "Failure Triggered"}}
</span>
<div class="bg-white p-3 small mt-2">
<div v-if="log.message !== ''" class="bg-white p-3 small mt-2">
<code>{{log.message}}</code>
</div>

View File

@ -1,5 +1,5 @@
import Vue from "vue";
const { startOfDay, startOfWeek, endOfMonth, startOfToday, startOfTomorrow, startOfYesterday, endOfYesterday, endOfTomorrow, endOfToday, endOfDay, startOfMonth, lastDayOfMonth, subSeconds, getUnixTime, fromUnixTime, differenceInSeconds, formatDistance, addMonths, addSeconds, isWithinInterval } = require('date-fns')
const { startOfDay, startOfHour, startOfWeek, endOfMonth, endOfHour, startOfToday, startOfTomorrow, startOfYesterday, endOfYesterday, endOfTomorrow, endOfToday, endOfDay, startOfMonth, lastDayOfMonth, subSeconds, getUnixTime, fromUnixTime, differenceInSeconds, formatDistance, addMonths, addSeconds, isWithinInterval } = require('date-fns')
import formatDistanceToNow from 'date-fns/formatDistanceToNow'
import format from 'date-fns/format'
import parseISO from 'date-fns/parseISO'
@ -59,6 +59,8 @@ export default Vue.mixin({
},
endOf(method, val) {
switch (method) {
case "hour":
return endOfHour(val)
case "day":
return endOfDay(val)
case "today":
@ -70,10 +72,17 @@ export default Vue.mixin({
case "month":
return endOfMonth(val)
}
return roundToNearestMinutes(val)
return val
},
startEndParams(start, end, group) {
start = this.beginningOf("hour", start)
end = this.endOf("hour", end)
return {start: this.toUnix(start), end: this.toUnix(end), group: group}
},
beginningOf(method, val) {
switch (method) {
case "hour":
return startOfHour(val)
case "day":
return startOfDay(val)
case "today":
@ -83,11 +92,11 @@ export default Vue.mixin({
case "yesterday":
return startOfYesterday()
case "week":
return startOfWeek()
return startOfWeek(val)
case "month":
return startOfMonth(val)
}
return roundToNearestMinutes(val)
return val
},
isZero(val) {
return getUnixTime(parseISO(val)) <= 0

View File

@ -1,16 +1,20 @@
<template>
<div class="container col-md-7 col-sm-12 mt-md-5">
<div v-if="modal" class="modal-backdrop"></div>
<Modal/>
<TopNav :admin="admin"/>
<router-view :admin="admin"/>
</div>
</template>
<script>
import Modal from "@/components/Elements/Modal";
const TopNav = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/TopNav')
export default {
name: 'Dashboard',
components: {
Modal,
TopNav,
},
data () {
@ -20,6 +24,9 @@
}
},
computed: {
modal() {
return this.$store.getters.modal.visible
},
admin() {
return this.$store.getters.admin
},

View File

@ -2275,7 +2275,7 @@ OluFxewsEO0QNDrfFb+0gnjYlnGqOFcZjUMXbDdY5oLSPtXohynuTK1qyQ==
</div>
<div class="text-center small text-dim" v-pre>
Automatically generated from Statping's Wiki on 2020-08-22 21:27:09.3468 &#43;0000 UTC
Automatically generated from Statping's Wiki on 2020-09-02 02:46:04.864615 &#43;0000 UTC
</div>
</div>

View File

@ -23,10 +23,7 @@
</div>
</div>
<div v-if="loaded">
<Group v-for="group in groups" v-bind:key="group.id" :group=group />
</div>
<Group v-for="group in groups" v-bind:key="group.id" :group=group />
<div class="col-12 full-col-12">
<MessageBlock v-for="message in messages" v-bind:key="message.id" :message="message" />
</div>
@ -66,20 +63,18 @@ export default {
},
computed: {
loading_text() {
if (this.core == null) {
if (!this.$store.getters.core.version) {
return "Loading Core"
} else if (this.groups == null) {
} else if (this.$store.getters.groups.length === 0) {
return "Loading Groups"
} else if (this.services == null) {
} else if (this.$store.getters.services.length === 0) {
return "Loading Services"
} else if (this.messages == null) {
} else if (this.$store.getters.messages == null) {
return "Loading Announcements"
} else {
return "Completed"
}
},
loaded() {
return this.core !== null && this.groups !== null && this.services !== null
return this.$store.getters.core.version && this.$store.getters.services.length !== 0
},
core() {
return this.$store.getters.core

View File

@ -2,7 +2,7 @@
<div class="offset-md-3 offset-lg-4 offset-0 col-lg-4 col-md-6 mt-5">
<div class="offset-1 offset-lg-2 col-lg-8 col-10 mb-4 mb-md-3">
<img alt="Statping Login" class="embed-responsive" src="http://0.0.0.0:8585/banner.png">
<img alt="Statping Login" class="embed-responsive" src="banner.png">
</div>
<div class="login_container col-12 p-4">

View File

@ -189,15 +189,23 @@
liClass(id) {
return this.tab === id
},
async renew() {
await Api.renewApiKeys()
const core = await Api.core()
this.$store.commit('setCore', core)
this.core = core
await this.logout()
},
async renewApiKeys() {
let r = confirm("Are you sure you want to reset the API keys? You will be logged out.");
if (r === true) {
await Api.renewApiKeys()
const core = await Api.core()
this.$store.commit('setCore', core)
this.core = core
await this.logout()
const modal = {
visible: true,
title: "Reset API Key",
body: `Are you sure you want to reset the API keys? You will be logged out.`,
btnColor: "btn-danger",
btnText: "Reset",
func: () => this.renew(),
}
this.$store.commit("setModal", modal)
},
async logout () {
await Api.logout()

View File

@ -31,7 +31,15 @@ export default new Vuex.Store({
checkins: [],
admin: false,
user: false,
loggedIn: false
loggedIn: false,
modal: {
visible: false,
title: "Modal Header",
body: "This is the content for the modal body",
btnText: "Save Changes",
btnColor: "btn-primary",
func: null,
}
},
getters: {
hasAllData: state => state.hasAllData,
@ -49,6 +57,7 @@ export default new Vuex.Store({
notifiers: state => state.notifiers,
checkins: state => state.checkins,
loggedIn: state => state.loggedIn,
modal: state => state.modal,
isAdmin: state => state.admin,
isUser: state => state.user,
@ -140,6 +149,9 @@ export default new Vuex.Store({
setOAuth(state, oauth) {
state.oauth = oauth
},
setModal(state, modal) {
state.modal = modal
},
},
actions: {
async getAllServices(context) {

1
go.mod
View File

@ -30,6 +30,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.3
github.com/statping/emails v1.0.0
github.com/stretchr/testify v1.5.1
github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1
github.com/tdewolff/minify/v2 v2.8.0 // indirect

3
go.sum
View File

@ -568,6 +568,9 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
github.com/statping/emails v1.0.0 h1:90hGweEhr8wIFiy34KCkiFqGJlkug2gAQLVR6oSCFNU=
github.com/statping/emails v1.0.0/go.mod h1:xFU85jXaiWQadqHqu/jDrGsAn6WPSk1WgKyTVuFm0TI=
github.com/statping/statping v0.90.64/go.mod h1:lbyNPB73IjWtnommV4wSejYfgUT1yLhhqelMjl1ZBb8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@ -31,10 +31,12 @@ func apiIndexHandler(r *http.Request) interface{} {
}
func apiRenewHandler(w http.ResponseWriter, r *http.Request) {
var err error
core.App.ApiSecret = utils.NewSHA256Hash()
err = core.App.Update()
if err != nil {
newApi := utils.Params.GetString("API_SECRET")
if newApi == "" {
newApi = utils.NewSHA256Hash()
}
core.App.ApiSecret = newApi
if err := core.App.Update(); err != nil {
sendErrorJson(err, w, r)
return
}

View File

@ -13,9 +13,14 @@ import (
func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) {
var notifs []notifications.Notification
for _, n := range services.AllNotifiers() {
no := n.Select()
notif, _ := notifications.Find(no.Method)
notifs = append(notifs, *no.UpdateFields(notif))
notif := n.Select()
no, err := notifications.Find(notif.Method)
if err != nil {
log.Error(err)
sendErrorJson(err, w, r)
}
notif.UpdateFields(no)
notifs = append(notifs, *notif)
}
sort.Sort(notifications.NotificationOrder(notifs))
returnJson(notifs, w, r)

View File

@ -132,6 +132,7 @@ func Router() *mux.Router {
api.Handle("/api/services/{id}/failures", scoped(apiServiceFailuresHandler)).Methods("GET")
api.Handle("/api/services/{id}/failures", authenticated(servicesDeleteFailuresHandler, false)).Methods("DELETE")
api.Handle("/api/services/{id}/hits", scoped(apiServiceHitsHandler)).Methods("GET")
api.Handle("/api/services/{id}/hits", authenticated(apiServiceHitsDeleteHandler, false)).Methods("DELETE")
// API SERVICE CHART DATA Routes
api.Handle("/api/services/{id}/hits_data", cached("30s", "application/json", apiServiceDataHandler)).Methods("GET")

View File

@ -237,6 +237,19 @@ func apiServiceTimeDataHandler(w http.ResponseWriter, r *http.Request) {
returnJson(uptimeData, w, r)
}
func apiServiceHitsDeleteHandler(w http.ResponseWriter, r *http.Request) {
service, err := findService(r)
if err != nil {
sendErrorJson(err, w, r)
return
}
if err := service.AllHits().DeleteAll(); err != nil {
sendErrorJson(err, w, r)
return
}
sendJsonAction(service, "delete", w, r)
}
func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
service, err := findService(r)
if err != nil {

View File

@ -64,7 +64,7 @@ var AmazonSNS = &amazonSNS{&notifications.Notification{
Type: "text",
Title: "SNS Topic ARN",
SmallText: "The ARN of the Topic",
DbField: "host",
DbField: "Host",
Placeholder: "arn:aws:sns:us-west-2:123456789012:YourTopic",
Required: true,
}}},

View File

@ -27,16 +27,15 @@ var Discorder = &discord{&notifications.Notification{
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(5 * time.Second),
Host: null.NewNullString("https://discordapp.com/api/webhooks/****/*****"),
Icon: "fab fa-discord",
SuccessData: null.NewNullString(`{"content": "Your service '{{.Service.Name}}' is currently online!"}`),
FailureData: null.NewNullString(`{"content": "Your service '{{.Service.Name}}' is currently failing! Reason: {{.Failure.Issue}}"}`),
SuccessData: null.NewNullString(`{"content": "Your service '{{.Service.Name}}' is currently back online and was down for {{.Service.Downtime.Human}}."}`),
FailureData: null.NewNullString(`{"content": "Your service '{{.Service.Name}}' is has been failing for {{.Service.Downtime.Human}}! Reason: {{.Failure.Issue}}"}`),
DataType: "json",
Limits: 60,
Form: []notifications.NotificationForm{{
Type: "text",
Title: "discord webhooker URL",
Placeholder: "Insert your Webhook URL here",
Placeholder: "https://discordapp.com/api/webhooks/****/*****",
DbField: "host",
}}},
}

View File

@ -1,17 +1,16 @@
package notifiers
import (
"bytes"
"crypto/tls"
"fmt"
"github.com/go-mail/mail"
"github.com/statping/emails"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/notifier"
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"html/template"
)
var _ notifier.Notifier = (*emailer)(nil)
@ -92,7 +91,7 @@ type emailOutgoing struct {
// OnFailure will trigger failing service
func (e *emailer) OnFailure(s services.Service, f failures.Failure) (string, error) {
subject := fmt.Sprintf("Service %s is Offline", s.Name)
tmpl := renderEmail(s, f, emailFailure)
tmpl := renderEmail(s, f, emails.Failure)
email := &emailOutgoing{
To: e.Var2.String,
Subject: subject,
@ -105,7 +104,7 @@ func (e *emailer) OnFailure(s services.Service, f failures.Failure) (string, err
// OnSuccess will trigger successful service
func (e *emailer) OnSuccess(s services.Service) (string, error) {
subject := fmt.Sprintf("Service %s is Back Online", s.Name)
tmpl := renderEmail(s, failures.Failure{}, emailSuccess)
tmpl := renderEmail(s, failures.Failure{}, emails.Success)
email := &emailOutgoing{
To: e.Var2.String,
Subject: subject,
@ -116,27 +115,18 @@ func (e *emailer) OnSuccess(s services.Service) (string, error) {
}
func renderEmail(s services.Service, f failures.Failure, emailData string) string {
wr := bytes.NewBuffer(nil)
tmpl := template.New("email")
tmpl, err := tmpl.Parse(emailData)
if err != nil {
log.Errorln(err)
return emailData
}
data := replacer{
Core: *core.App,
Service: s,
Failure: f,
Custom: nil,
}
if err = tmpl.ExecuteTemplate(wr, "email", data); err != nil {
output, err := emails.Parse(emailData, data)
if err != nil {
log.Errorln(err)
return emailData
}
return wr.String()
return output
}
// OnTest triggers when this notifier has been saved

View File

@ -37,6 +37,8 @@ func InitNotifiers() {
Gotify,
AmazonSNS,
)
services.UpdateNotifiers()
}
func ReplaceTemplate(tmpl string, data replacer) string {
@ -56,10 +58,11 @@ func ReplaceTemplate(tmpl string, data replacer) string {
func Add(notifs ...services.ServiceNotifier) {
for _, n := range notifs {
services.AddNotifier(n)
if err := n.Select().Create(); err != nil {
notif := n.Select()
if err := notif.Create(); err != nil {
log.Error(err)
}
services.AddNotifier(n)
}
}

View File

@ -9,7 +9,6 @@ import (
"github.com/statping/statping/types/null"
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"regexp"
"strings"
"time"
)
@ -94,10 +93,5 @@ func (s *slack) OnSave() (string, error) {
}
func (s *slack) Valid(values notifications.Values) error {
regex := `https\:\/\/hooks\.slack\.com/services/[A-Z0-9]{7,11}/[A-Z0-9]{7,11}/[a-zA-Z0-9]{20,22}`
r := regexp.MustCompile(regex)
if !r.MatchString(values.Host) {
return errors.New("slack webhook does not match with expected regex " + regex)
}
return nil
}

View File

@ -40,7 +40,7 @@ func TestSlackNotifier(t *testing.T) {
t.Run("Load slack", func(t *testing.T) {
slacker.Host = null.NewNullString(SLACK_URL)
slacker.Delay = time.Duration(100 * time.Millisecond)
slacker.Delay = 100 * time.Millisecond
slacker.Limits = 3
Add(slacker)
assert.Equal(t, "Hunter Long", slacker.Author)

View File

@ -8,7 +8,7 @@ func (c *Checkin) LastHit() *CheckinHit {
func (c *Checkin) Hits() []*CheckinHit {
var hits []*CheckinHit
dbHits.Where("checkin = ?", c.Id).Order("DESC").Find(&hits)
dbHits.Where("checkin = ?", c.Id).Order("id DESC").Find(&hits)
c.AllHits = hits
return hits
}

View File

@ -48,21 +48,21 @@ func LoadConfigForm(r *http.Request) (*DbConfig, error) {
p.Set("ADMIN_EMAIL", email)
confg := &DbConfig{
DbConn: dbConn,
DbHost: dbHost,
DbUser: dbUser,
DbPass: dbPass,
DbData: dbDatabase,
DbPort: int(dbPort),
Project: project,
Description: description,
Domain: domain,
Username: username,
Password: password,
Email: email,
Location: utils.Directory,
Language: language,
SendReports: reports,
DbConn: dbConn,
DbHost: dbHost,
DbUser: dbUser,
DbPass: dbPass,
DbData: dbDatabase,
DbPort: int(dbPort),
Project: project,
Description: description,
Domain: domain,
Username: username,
Password: password,
Email: email,
Location: utils.Directory,
Language: language,
AllowReports: reports,
}
return confg, nil

View File

@ -59,7 +59,7 @@ func LoadConfigs(cfgFile string) (*DbConfig, error) {
if db.Language != "" {
p.Set("LANGUAGE", db.Language)
}
if db.SendReports {
if db.AllowReports {
p.Set("ALLOW_REPORTS", true)
}
if db.LetsEncryptEmail != "" {
@ -88,11 +88,12 @@ func LoadConfigs(cfgFile string) (*DbConfig, error) {
Location: utils.Directory,
SqlFile: p.GetString("SQL_FILE"),
Language: p.GetString("LANGUAGE"),
SendReports: p.GetBool("ALLOW_REPORTS"),
AllowReports: p.GetBool("ALLOW_REPORTS"),
LetsEncryptEnable: p.GetBool("LETSENCRYPT_ENABLE"),
LetsEncryptHost: p.GetString("LETSENCRYPT_HOST"),
LetsEncryptEmail: p.GetString("LETSENCRYPT_EMAIL"),
ApiSecret: p.GetString("API_SECRET"),
SampleData: p.GetBool("SAMPLE_DATA"),
}
log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + cfgFile)

View File

@ -14,7 +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"`
AllowReports bool `yaml:"allow_reports" json:"allow_reports"`
Project string `yaml:"-" json:"-"`
Description string `yaml:"-" json:"-"`
Domain string `yaml:"-" json:"-"`
@ -26,8 +26,28 @@ type DbConfig struct {
SqlFile string `yaml:"sqlfile,omitempty" json:"-"`
LetsEncryptHost string `yaml:"letsencrypt_host,omitempty" json:"letsencrypt_host"`
LetsEncryptEmail string `yaml:"letsencrypt_email,omitempty" json:"letsencrypt_email"`
LetsEncryptEnable bool `yaml:"letsencrypt_enable" json:"letsencrypt_enable"`
LetsEncryptEnable bool `yaml:"letsencrypt_enable,omitempty" json:"letsencrypt_enable"`
LocalIP string `yaml:"-" json:"-"`
DisableHTTP bool `yaml:"disable_http" json:"disable_http"`
DemoMode bool `yaml:"demo_mode" json:"demo_mode"`
DisableLogs bool `yaml:"disable_logs" json:"disable_logs"`
UseAssets bool `yaml:"use_assets" json:"use_assets"`
BasePath string `yaml:"base_path" json:"base_path"`
AdminUser string `yaml:"admin_user" json:"admin_user"`
AdminPassword string `yaml:"admin_password" json:"admin_password"`
AdminEmail string `yaml:"admin_email" json:"admin_email"`
MaxOpenConnections int `yaml:"db_open_connections" json:"db_open_connections"`
MaxIdleConnections int `yaml:"db_idle_connections" json:"db_idle_connections"`
MaxLifeConnections int `yaml:"db_max_life_connections" json:"db_max_life_connections"`
SampleData bool `yaml:"sample_data" json:"sample_data"`
UseCDN bool `yaml:"use_cdn" json:"use_cdn"`
DisableColors bool `yaml:"disable_colors" json:"disable_colors"`
PostgresSSLMode string `yaml:"postgres_ssl" json:"postgres_ssl"`
Db database.Database `yaml:"-" json:"-"`
}

View File

@ -34,7 +34,7 @@ func (i *Incident) BeforeCreate() error {
}
func (i *Incident) AfterFind() {
db.Model(i).Related(&i.Updates).Order("DESC")
db.Model(i).Related(&i.Updates).Order("id DESC")
metrics.Query("incident", "find")
}

View File

@ -25,6 +25,15 @@ func (n *Notification) Values() Values {
}
}
func All() []*Notification {
var n []*Notification
q := db.Find(&n)
if q.Error() != nil {
return nil
}
return n
}
func Find(method string) (*Notification, error) {
var n Notification
q := db.Where("method = ?", method).Find(&n)
@ -38,6 +47,7 @@ func (n *Notification) Create() error {
var p Notification
q := db.Where("method = ?", n.Method).Find(&p)
if q.RecordNotFound() {
log.Infof("Notifier '%s' was not found, adding into database...\n", n.Method)
if err := db.Create(n).Error(); err != nil {
return err
}
@ -56,6 +66,9 @@ func (n *Notification) Create() error {
}
func (n *Notification) UpdateFields(notif *Notification) *Notification {
if notif == nil {
return n
}
n.Id = notif.Id
n.Limits = notif.Limits
n.Enabled = notif.Enabled

View File

@ -38,12 +38,10 @@ type Notification struct {
AuthorUrl string `gorm:"-" json:"author_url"`
Icon string `gorm:"-" json:"icon"`
Delay time.Duration `gorm:"-" json:"delay,string"`
Running chan bool `gorm:"-" json:"-"`
Form []NotificationForm `gorm:"-" json:"form"`
LastSent time.Time `gorm:"-" json:"-"`
LastSentCount int `gorm:"-" json:"-"`
sentCount int `gorm:"-" json:"-"`
Logs []*NotificationLog `gorm:"-" json:"logs,omitempty"`
}
@ -59,8 +57,6 @@ func (n *Notification) Logger() *logrus.Logger {
return log.WithField("notifier", n.Method).Logger
}
type RunFunc func(interface{}) error
type Values struct {
Host string
Port int64

View File

@ -1,10 +1,15 @@
package null
import (
"database/sql/driver"
"encoding/json"
"gopkg.in/yaml.v2"
)
func (s NullString) Value() (driver.Value, error) {
return s.String, nil
}
// MarshalJSON for NullInt64
func (i NullInt64) MarshalJSON() ([]byte, error) {
if !i.Valid {
@ -32,7 +37,7 @@ func (bb NullBool) MarshalJSON() ([]byte, error) {
// MarshalJSON for NullString
func (s NullString) MarshalJSON() ([]byte, error) {
if !s.Valid {
return []byte("null"), nil
return json.Marshal(nil)
}
return json.Marshal(s.String)
}

View File

@ -11,6 +11,13 @@ func AddNotifier(n ServiceNotifier) {
allNotifiers[notif.Method] = n
}
func UpdateNotifiers() {
for _, n := range notifications.All() {
notifier := allNotifiers[n.Method]
notifier.Select().UpdateFields(n)
}
}
func sendSuccess(s *Service) {
if !s.AllowNotifications.Bool {
return

View File

@ -14,6 +14,15 @@ func (u *User) Validate() error {
return nil
}
func (u *User) BeforeDelete() error {
if utils.Params.GetBool("DEMO_MODE") {
if u.Username == "admin" {
return errors.New("cannot delete admin in DEMO_MODE")
}
}
return nil
}
func (u *User) BeforeCreate() error {
if err := u.Validate(); err != nil {
return err

View File

@ -1,5 +0,0 @@
package utils
var (
StartTime = Now()
)

View File

@ -192,7 +192,8 @@ func TestHttpRequest(t *testing.T) {
}
func TestConfigLoad(t *testing.T) {
InitLogs()
err := InitLogs()
require.Nil(t, err)
InitEnvs()
s := Params.GetString

View File

@ -1 +1 @@
0.90.64
0.90.65