* 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 API_SECRET: demopassword123
DISABLE_LOGS: false DISABLE_LOGS: false
ALLOW_REPORTS: true ALLOW_REPORTS: true
SAMPLE_DATA: true
COVERALLS: ${{ secrets.COVERALLS }} COVERALLS: ${{ secrets.COVERALLS }}
DISCORD_URL: ${{ secrets.DISCORD_URL }} DISCORD_URL: ${{ secrets.DISCORD_URL }}
EMAIL_HOST: ${{ secrets.EMAIL_HOST }} EMAIL_HOST: ${{ secrets.EMAIL_HOST }}

View File

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

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ snap
prime prime
stage stage
parts parts
assets_backup
certs certs
releases releases
core/rice-box.go 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) # 0.90.64 (08-18-2020)
- Modified max-width for container to 1012px, larger UI - Modified max-width for container to 1012px, larger UI
- Added failure sparklines in the Services list view - Added failure sparklines in the Services list view

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,34 @@ A:HOVER {
color: lighten($text-color, 12%) !important; 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 { .text-muted {
color: lighten($text-color, 30%) !important; color: lighten($text-color, 30%) !important;
} }

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="row"> <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}} <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 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"> <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: { 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) { async deleteIncident(incident) {
let c = confirm(`Are you sure you want to delete '${incident.title}'?`) const modal = {
if (c) { visible: true,
this.res = await Api.incident_delete(incident) title: "Delete Incident",
if (this.res.status === "success") { body: `Are you sure you want to delete Incident ${incident.title}?`,
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 btnColor: "btn-danger",
//await this.loadIncidents() btnText: "Delete Incident",
} // TODO: further error checking here... maybe alert user it failed with modal or so func: () => this.delete(incident),
} }
this.$store.commit("setModal", modal)
}, },
async createIncident() { async createIncident() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@
<div class="form-group row"> <div class="form-group row">
<label for="password" class="col-4 col-form-label">{{$t('password')}}</label> <label for="password" class="col-4 col-form-label">{{$t('password')}}</label>
<div class="col-8"> <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> </div>
<div class="form-group row"> <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}"> <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"> <span class="d-block">
Service '{{$store.getters.serviceById(log.service).name}}' Service {{log.service}}
{{log.success ? "Success Triggered" : "Failure Triggered"}} {{log.success ? "Success Triggered" : "Failure Triggered"}}
</span> </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> <code>{{log.message}}</code>
</div> </div>

View File

@ -1,5 +1,5 @@
import Vue from "vue"; 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 formatDistanceToNow from 'date-fns/formatDistanceToNow'
import format from 'date-fns/format' import format from 'date-fns/format'
import parseISO from 'date-fns/parseISO' import parseISO from 'date-fns/parseISO'
@ -59,6 +59,8 @@ export default Vue.mixin({
}, },
endOf(method, val) { endOf(method, val) {
switch (method) { switch (method) {
case "hour":
return endOfHour(val)
case "day": case "day":
return endOfDay(val) return endOfDay(val)
case "today": case "today":
@ -70,10 +72,17 @@ export default Vue.mixin({
case "month": case "month":
return endOfMonth(val) 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) { beginningOf(method, val) {
switch (method) { switch (method) {
case "hour":
return startOfHour(val)
case "day": case "day":
return startOfDay(val) return startOfDay(val)
case "today": case "today":
@ -83,11 +92,11 @@ export default Vue.mixin({
case "yesterday": case "yesterday":
return startOfYesterday() return startOfYesterday()
case "week": case "week":
return startOfWeek() return startOfWeek(val)
case "month": case "month":
return startOfMonth(val) return startOfMonth(val)
} }
return roundToNearestMinutes(val) return val
}, },
isZero(val) { isZero(val) {
return getUnixTime(parseISO(val)) <= 0 return getUnixTime(parseISO(val)) <= 0

View File

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

View File

@ -2275,7 +2275,7 @@ OluFxewsEO0QNDrfFb+0gnjYlnGqOFcZjUMXbDdY5oLSPtXohynuTK1qyQ==
</div> </div>
<div class="text-center small text-dim" v-pre> <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>
</div> </div>

View File

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

View File

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

View File

@ -31,7 +31,15 @@ export default new Vuex.Store({
checkins: [], checkins: [],
admin: false, admin: false,
user: 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: { getters: {
hasAllData: state => state.hasAllData, hasAllData: state => state.hasAllData,
@ -49,6 +57,7 @@ export default new Vuex.Store({
notifiers: state => state.notifiers, notifiers: state => state.notifiers,
checkins: state => state.checkins, checkins: state => state.checkins,
loggedIn: state => state.loggedIn, loggedIn: state => state.loggedIn,
modal: state => state.modal,
isAdmin: state => state.admin, isAdmin: state => state.admin,
isUser: state => state.user, isUser: state => state.user,
@ -140,6 +149,9 @@ export default new Vuex.Store({
setOAuth(state, oauth) { setOAuth(state, oauth) {
state.oauth = oauth state.oauth = oauth
}, },
setModal(state, modal) {
state.modal = modal
},
}, },
actions: { actions: {
async getAllServices(context) { async getAllServices(context) {

1
go.mod
View File

@ -30,6 +30,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.3 github.com/spf13/viper v1.6.3
github.com/statping/emails v1.0.0
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1 github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1
github.com/tdewolff/minify/v2 v2.8.0 // indirect 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.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 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= 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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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) { func apiRenewHandler(w http.ResponseWriter, r *http.Request) {
var err error newApi := utils.Params.GetString("API_SECRET")
core.App.ApiSecret = utils.NewSHA256Hash() if newApi == "" {
err = core.App.Update() newApi = utils.NewSHA256Hash()
if err != nil { }
core.App.ApiSecret = newApi
if err := core.App.Update(); err != nil {
sendErrorJson(err, w, r) sendErrorJson(err, w, r)
return return
} }

View File

@ -13,9 +13,14 @@ import (
func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) { func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) {
var notifs []notifications.Notification var notifs []notifications.Notification
for _, n := range services.AllNotifiers() { for _, n := range services.AllNotifiers() {
no := n.Select() notif := n.Select()
notif, _ := notifications.Find(no.Method) no, err := notifications.Find(notif.Method)
notifs = append(notifs, *no.UpdateFields(notif)) if err != nil {
log.Error(err)
sendErrorJson(err, w, r)
}
notif.UpdateFields(no)
notifs = append(notifs, *notif)
} }
sort.Sort(notifications.NotificationOrder(notifs)) sort.Sort(notifications.NotificationOrder(notifs))
returnJson(notifs, w, r) 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", scoped(apiServiceFailuresHandler)).Methods("GET")
api.Handle("/api/services/{id}/failures", authenticated(servicesDeleteFailuresHandler, false)).Methods("DELETE") 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", scoped(apiServiceHitsHandler)).Methods("GET")
api.Handle("/api/services/{id}/hits", authenticated(apiServiceHitsDeleteHandler, false)).Methods("DELETE")
// API SERVICE CHART DATA Routes // API SERVICE CHART DATA Routes
api.Handle("/api/services/{id}/hits_data", cached("30s", "application/json", apiServiceDataHandler)).Methods("GET") 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) 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) { func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
service, err := findService(r) service, err := findService(r)
if err != nil { if err != nil {

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,6 @@ import (
"github.com/statping/statping/types/null" "github.com/statping/statping/types/null"
"github.com/statping/statping/types/services" "github.com/statping/statping/types/services"
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
"regexp"
"strings" "strings"
"time" "time"
) )
@ -94,10 +93,5 @@ func (s *slack) OnSave() (string, error) {
} }
func (s *slack) Valid(values notifications.Values) 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 return nil
} }

View File

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

View File

@ -8,7 +8,7 @@ func (c *Checkin) LastHit() *CheckinHit {
func (c *Checkin) Hits() []*CheckinHit { func (c *Checkin) Hits() []*CheckinHit {
var 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 c.AllHits = hits
return hits return hits
} }

View File

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

View File

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

View File

@ -14,7 +14,7 @@ type DbConfig struct {
DbPort int `yaml:"port" json:"-"` DbPort int `yaml:"port" json:"-"`
ApiSecret string `yaml:"api_secret" json:"-"` ApiSecret string `yaml:"api_secret" json:"-"`
Language string `yaml:"language" json:"language"` 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:"-"` Project string `yaml:"-" json:"-"`
Description string `yaml:"-" json:"-"` Description string `yaml:"-" json:"-"`
Domain string `yaml:"-" json:"-"` Domain string `yaml:"-" json:"-"`
@ -26,8 +26,28 @@ type DbConfig struct {
SqlFile string `yaml:"sqlfile,omitempty" json:"-"` SqlFile string `yaml:"sqlfile,omitempty" json:"-"`
LetsEncryptHost string `yaml:"letsencrypt_host,omitempty" json:"letsencrypt_host"` LetsEncryptHost string `yaml:"letsencrypt_host,omitempty" json:"letsencrypt_host"`
LetsEncryptEmail string `yaml:"letsencrypt_email,omitempty" json:"letsencrypt_email"` 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:"-"` 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:"-"` Db database.Database `yaml:"-" json:"-"`
} }

View File

@ -34,7 +34,7 @@ func (i *Incident) BeforeCreate() error {
} }
func (i *Incident) AfterFind() { 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") 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) { func Find(method string) (*Notification, error) {
var n Notification var n Notification
q := db.Where("method = ?", method).Find(&n) q := db.Where("method = ?", method).Find(&n)
@ -38,6 +47,7 @@ func (n *Notification) Create() error {
var p Notification var p Notification
q := db.Where("method = ?", n.Method).Find(&p) q := db.Where("method = ?", n.Method).Find(&p)
if q.RecordNotFound() { if q.RecordNotFound() {
log.Infof("Notifier '%s' was not found, adding into database...\n", n.Method)
if err := db.Create(n).Error(); err != nil { if err := db.Create(n).Error(); err != nil {
return err return err
} }
@ -56,6 +66,9 @@ func (n *Notification) Create() error {
} }
func (n *Notification) UpdateFields(notif *Notification) *Notification { func (n *Notification) UpdateFields(notif *Notification) *Notification {
if notif == nil {
return n
}
n.Id = notif.Id n.Id = notif.Id
n.Limits = notif.Limits n.Limits = notif.Limits
n.Enabled = notif.Enabled n.Enabled = notif.Enabled

View File

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

View File

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

View File

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

View File

@ -14,6 +14,15 @@ func (u *User) Validate() error {
return nil 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 { func (u *User) BeforeCreate() error {
if err := u.Validate(); err != nil { if err := u.Validate(); err != nil {
return err 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) { func TestConfigLoad(t *testing.T) {
InitLogs() err := InitLogs()
require.Nil(t, err)
InitEnvs() InitEnvs()
s := Params.GetString s := Params.GetString

View File

@ -1 +1 @@
0.90.64 0.90.65