pull/429/head
Hunter Long 2020-01-16 01:29:49 -08:00
parent 30e64f688c
commit 978bc17768
26 changed files with 2745 additions and 157 deletions

View File

@ -20,6 +20,9 @@ release: dev-deps
frontend:
cd frontend && yarn serve
frontend-build:
cd frontend && rm -rf dist && yarn build
# build and push the images to docker hub
docker: docker-build-all docker-publish-all

View File

@ -11,6 +11,7 @@
"apexcharts": "^3.15.0",
"axios": "^0.19.1",
"core-js": "^3.4.4",
"querystring": "^0.2.0",
"vue": "^2.6.10",
"vue-apexcharts": "^1.5.2",
"vue-router": "^3.1.3"

View File

@ -1,13 +1,14 @@
<template>
<div id="app">
<router-view/>
<Footer/>
<Footer version="DEV" />
</div>
</template>
<script>
import Footer from "./components/Footer";
export default {
import Footer from "./components/Footer";
export default {
name: 'app',
components: {
Footer

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@ -1,5 +1,8 @@
import axios from 'axios'
const qs = require('querystring')
const tokenKey = "statping_user";
class Api {
constructor() {
@ -21,10 +24,52 @@ class Api {
return axios.get('/api/groups').then(response => (response.data))
}
async users () {
return axios.get('/api/users').then(response => (response.data))
}
async messages () {
return axios.get('/api/messages').then(response => (response.data))
}
async group (id) {
return axios.get('/api/groups/'+id).then(response => (response.data))
}
async notifiers () {
return axios.get('/api/notifiers').then(response => (response.data))
}
async login (username, password) {
const f = {username: username, password: password}
return axios.post('/api/login', qs.stringify(f))
.then(response => (response.data))
}
async logout () {
await axios.get('/api/logout').then(response => (response.data))
return localStorage.removeItem(tokenKey)
}
saveToken (username, token) {
const user = {username: username, token: token}
localStorage.setItem(tokenKey, JSON.stringify(user));
return user
}
token () {
return JSON.parse(localStorage.getItem(tokenKey));
}
authToken () {
let user = JSON.parse(localStorage.getItem(tokenKey));
if (user && user.token) {
return { 'Authorization': 'Bearer ' + user.token };
} else {
return {};
}
}
}
const api = new Api()
export default api

View File

@ -0,0 +1,44 @@
<template>
<div class="col-12 mt-3">
<div class="row stats_area mb-5">
<div class="col-4">
<span class="lg_number">{{services.length}}</span>
Total Services
</div>
<div class="col-4">
<span class="lg_number">48</span>
Failures last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">7</span>
Online Services
</div>
</div>
<div v-for="(service, index) in services" v-bind:key="index">
<ServiceInfo :service=service />
</div>
</div>
</template>
<script>
import ServiceInfo from "./ServiceInfo";
export default {
name: 'DashboardIndex',
components: {
ServiceInfo
},
props: {
services: Array
},
methods: {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -0,0 +1,181 @@
<!--
- Statup
- Copyright (C) 2020. Hunter Long and the project contributors
- Written by Hunter Long <info@socialeck.com> and the project contributors
-
- https://github.com/hunterlong/statup
-
- The licenses for most software and other practical works are designed
- to take away your freedom to share and change the works. By contrast,
- the GNU General Public License is intended to guarantee your freedom to
- share and change all versions of a program--to make sure it remains free
- software for all its users.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div>
<div class="col-12">
<h1 class="text-black-50">Messages</h1>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col" class="d-none d-md-table-cell">Service</th>
<th scope="col" class="d-none d-md-table-cell">Begins</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="(message, index) in messages" v-bind:key="index">
<td>{{message.title}}</td>
<td class="d-none d-md-table-cell"><a href="service/1">{{message.service}}</a></td>
<td class="d-none d-md-table-cell">{{message.start_on}}</td>
<td class="text-right">
<div class="btn-group">
<a href="message/1" class="btn btn-outline-secondary"><i class="fas fa-exclamation-triangle"></i> Edit</a>
<a href="api/messages/1" class="ajax_delete btn btn-danger"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-12">
<h1 class="text-black-50 mt-5">Create Message</h1>
<div class="card">
<div class="card-body">
<form class="ajax_form" action="api/messages" data-redirect="messages" method="POST">
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">
<input type="text" name="title" class="form-control" value="" id="title" placeholder="Message Title" required>
</div>
</div>
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Description</label>
<div class="col-sm-8">
<textarea rows="5" name="description" class="form-control" id="description" required></textarea>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Message Date Range</label>
<div class="col-sm-4">
<input type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="0001-01-01T00:00:00Z" required>
</div>
<div class="col-sm-4">
<input type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="0001-01-01T00:00:00Z" required>
</div>
</div>
<div class="form-group row">
<label for="service_id" class="col-sm-4 col-form-label">Service</label>
<div class="col-sm-8">
<select class="form-control" name="service" id="service_id">
<option value="0" selected>Global Message</option>
<option value="7" >Statping API</option>
<option value="6" >Push Notification Server</option>
<option value="1" >Google</option>
<option value="2" >Statping Github</option>
<option value="3" >JSON Users Test</option>
<option value="4" >JSON API Tester</option>
<option value="5" >Google DNS</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notification Method</label>
<div class="col-sm-8">
<input type="text" name="notify_method" class="form-control" id="notify_method" value="" placeholder="email">
</div>
</div>
<div class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notify Users</label>
<div class="col-sm-8">
<span class="switch">
<input type="checkbox" name="notify_users-value" class="switch" id="switch-normal">
<label for="switch-normal">Notify Users Before Scheduled Time</label>
<input type="hidden" name="notify_users" id="switch-normal-value" value="false">
</span>
</div>
</div>
<div class="form-group row">
<label for="notify_before" class="col-sm-4 col-form-label">Notify Before</label>
<div class="col-sm-8">
<div class="form-inline">
<input type="number" name="notify_before" class="col-4 form-control" id="notify_before" value="0">
<select class="ml-2 col-7 form-control" name="notify_before_scale" id="notify_before_scale">
<option value="minute">Minutes</option>
<option value="hour">Hours</option>
<option value="day">Days</option>
</select>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block">Create Message</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import Api from "../API"
export default {
name: 'DashboardMessages',
data () {
return {
messages: null
}
},
created() {
this.getMessages()
},
methods: {
async getMessages () {
this.messages = await Api.messages()
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -0,0 +1,146 @@
<!--
- Statup
- Copyright (C) 2020. Hunter Long and the project contributors
- Written by Hunter Long <info@socialeck.com> and the project contributors
-
- https://github.com/hunterlong/statup
-
- The licenses for most software and other practical works are designed
- to take away your freedom to share and change the works. By contrast,
- the GNU General Public License is intended to guarantee your freedom to
- share and change all versions of a program--to make sure it remains free
- software for all its users.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div>
<div class="col-12">
<h1 class="text-black-50">Services <a href="service/create" class="btn btn-outline-success mt-1 float-right">
<i class="fas fa-plus"></i> Create</a>
</h1>
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col" class="d-none d-md-table-cell">Status</th>
<th scope="col" class="d-none d-md-table-cell">Visibility</th>
<th scope="col" class="d-none d-md-table-cell">Group</th>
<th scope="col"></th>
</tr>
</thead>
<tbody class="sortable" id="services_table">
<tr v-for="(service, index) in services" v-bind:key="index">
<td><span class="drag_icon d-none d-md-inline"><i class="fas fa-bars"></i></span> {{service.name}}</td>
<td class="d-none d-md-table-cell"><span class="badge badge-success">ONLINE</span>
<i class="toggle-service fas fa-toggle-on text-success" data-online="true" data-id="1"></i>
</td>
<td class="d-none d-md-table-cell"><span class="badge badge-primary">PUBLIC</span></td>
<td class="d-none d-md-table-cell"><span class="badge badge-secondary">Main Services</span></td>
<td class="text-right">
<div class="btn-group">
<router-link :to="{path: `/service/${service.id}`, params: {service: service} }" class="btn btn-outline-secondary"><i class="fas fa-chart-area"></i> View</router-link>
<a href="api/services/1" class="ajax_delete btn btn-danger" data-method="DELETE" data-obj="service_1" data-id="1"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-12 mt-5">
<h1 class="text-muted">Groups</h1>
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Services</th>
<th scope="col">Visibility</th>
<th scope="col"></th>
</tr>
</thead>
<tbody class="sortable_groups" id="groups_table">
<tr v-for="(group, index) in groups" v-bind:key="index">
<td><span class="drag_icon d-none d-md-inline"><i class="fas fa-bars"></i></span>{{group.name}}</td>
<td></td>
<td><span class="badge badge-secondary">PRIVATE</span></td>
<td class="text-right">
<div class="btn-group">
<a href="group/2" class="btn btn-outline-secondary"><i class="fas fa-chart-area"></i> Edit</a>
<a href="api/groups/2" class="ajax_delete btn btn-danger" data-method="DELETE" data-obj="group_2" data-id="2"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>
</tbody>
</table>
<h1 class="text-muted mt-5">Create Group</h1>
<div class="card">
<div class="card-body">
<form class="ajax_form" action="api/groups" data-redirect="services" method="POST">
<div class="form-group row">
<label for="title" class="col-sm-4 col-form-label">Group Name</label>
<div class="col-sm-8">
<input type="text" name="name" class="form-control" value="" id="title" placeholder="Group Name" required>
</div>
</div>
<div class="form-group row">
<label for="switch-group-public" class="col-sm-4 col-form-label">Public Group</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="public" class="switch" id="switch-group-public" >
<label for="switch-group-public">Show group services to the public</label>
<input type="hidden" name="public" id="switch-group-public-value" value="false">
</span>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block">Create Group</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DashboardServices',
props: {
services: Array,
groups: Array,
},
data () {
return {
}
},
beforeMount() {
},
methods: {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -0,0 +1,114 @@
<!--
- Statup
- Copyright (C) 2020. Hunter Long and the project contributors
- Written by Hunter Long <info@socialeck.com> and the project contributors
-
- https://github.com/hunterlong/statup
-
- The licenses for most software and other practical works are designed
- to take away your freedom to share and change the works. By contrast,
- the GNU General Public License is intended to guarantee your freedom to
- share and change all versions of a program--to make sure it remains free
- software for all its users.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="col-12">
<h1 class="text-black-50">Users</h1>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Username</th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="users_table">
<tr v-for="(user, index) in users" v-bind:key="index" >
<td>{{user.username}}</td>
<td class="text-right">
<div class="btn-group">
<a href="user/1" class="btn btn-outline-secondary"><i class="fas fa-user-edit"></i> Edit</a>
<a href="api/users/1" class="ajax_delete btn btn-danger"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>
</tbody>
</table>
<h1 class="text-black-50 mt-5">Create User</h1>
<div class="card">
<div class="card-body">
<form class="ajax_form" action="api/users" data-redirect="users" method="POST">
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Username</label>
<div class="col-6 col-md-4">
<input type="text" name="username" class="form-control" value="" id="username" placeholder="Username" required autocorrect="off" autocapitalize="none">
</div>
<div class="col-6 col-md-4">
<span class="switch">
<input type="checkbox" name="admin" class="switch" id="switch-normal">
<label for="switch-normal">Administrator</label>
<input type="hidden" name="admin" id="switch-normal-value" value="false">
</span>
</div>
</div>
<div class="form-group row">
<label for="email" class="col-sm-4 col-form-label">Email Address</label>
<div class="col-sm-8">
<input type="email" name="email" class="form-control" id="email" value="" placeholder="user@domain.com" required autocapitalize="none" spellcheck="false">
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-4 col-form-label">Password</label>
<div class="col-sm-8">
<input type="password" name="password" class="form-control" id="password" placeholder="Password" required>
</div>
</div>
<div class="form-group row">
<label for="password_confirm" class="col-sm-4 col-form-label">Confirm Password</label>
<div class="col-sm-8">
<input type="password" name="password_confirm" class="form-control" id="password_confirm" placeholder="Confirm Password" required>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block">Create User</button>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
</div>
</template>
<script>
import Api from "../API"
export default {
name: 'DashboardUsers',
data () {
return {
users: null
}
},
created() {
this.getUsers()
},
methods: {
async getUsers () {
this.users = await Api.users()
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -0,0 +1,36 @@
<template>
<div class="col-12 card mb-3" style="min-height: 260px">
<div class="card-body">
<h5 class="card-title"><a href="service/7">{{service.name}}</a>
<span class="badge float-right badge-success">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
</h5>
<div class="row">
<div class="col-md-3 col-sm-6">
<div id="spark_service_7_1"></div>
</div>
<div class="col-md-3 col-sm-6">
<div id="spark_service_7_2"></div>
</div>
<div class="col-md-3 col-sm-6">
<div id="spark_service_7_3"></div>
</div>
<div class="col-md-3 col-sm-6">
<div id="spark_service_7_4"></div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ServiceInfo',
props: {
service: Object
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -0,0 +1,61 @@
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<router-link to="/" class="navbar-brand">Statping</router-link>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a v-on:click="changeView('DashboardIndex', '/dashboard')" class="nav-link" href="#">Dashboard</a>
</li>
<li class="nav-item">
<a v-on:click="changeView('DashboardServices', '/dashboard/services')" class="nav-link" href="#">Services</a>
</li>
<li class="nav-item">
<a v-on:click="changeView('DashboardUsers', '/dashboard/users')" class="nav-link" href="#">Users</a>
</li>
<li class="nav-item">
<a v-on:click="changeView('DashboardMessages', '/dashboard/messages')" class="nav-link" href="#">Messages</a>
</li>
<li class="nav-item">
<a v-on:click="changeView('Settings', '/dashboard/settings')" class="nav-link" href="#">Settings</a>
</li>
<li class="nav-item">
<a v-on:click="changeView('Logs', '/dashboard/logs')" class="nav-link" href="#">Logs</a>
</li>
<li class="nav-item">
<a v-on:click="changeView('DashboardIndex', '/dashboard/help')" class="nav-link" href="#">Help</a>
</li>
</ul>
<span class="navbar-text">
<a href="#" class="nav-link" v-on:click="logout">Logout</a>
</span>
</div>
</nav>
</template>
<script>
import Api from "../API"
export default {
name: 'TopNav',
props: {
changeView: Function
},
methods: {
async logout () {
await Api.logout()
await this.$router.push('/')
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,14 +1,18 @@
<template>
<footer>
<div class="footer text-center mb-4 p-2">
<a href="https://github.com/hunterlong/statping" target="_blank">Statping 0.80.70 made with <i class="text-danger fas fa-heart"></i></a> | <a href="dashboard">Dashboard</a>
<a href="https://github.com/hunterlong/statping" target="_blank">Statping {{version}} made with <i class="text-danger fas fa-heart"></i></a> |
<router-link to="/dashboard">Dashboard</router-link>
</div>
</footer>
</template>
<script>
export default {
name: 'Footer'
name: 'Footer',
props: {
version: String
}
}
</script>

View File

@ -3,7 +3,8 @@
<div class="card">
<div class="card-body">
<div class="col-12">
<h4 class="mt-3"><a href="service/1">{{service.name}}</a>
<h4 class="mt-3">
<router-link :to="`/service/${service.id}`">{{service.name}}</router-link>
<span class="badge bg-success float-right">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
</h4>
@ -34,7 +35,7 @@
<span class="d-none d-md-inline">Online, last Failure was Wednesday 1:16:49PM, Dec 18 2019</span>
</div>
<div class="col-sm-12 col-md-2">
<a href="service/1" class="btn btn-success btn-sm float-right dyn-dark btn-block">View Service</a>
<router-link :to="`/service/${service.id}`" class="btn btn-success btn-sm float-right dyn-dark btn-block">View Service</router-link>
</div>
</div>

View File

@ -3,29 +3,43 @@ import App from './App.vue'
import VueRouter from 'vue-router'
import Index from "./pages/Index";
import Dashboard from "./pages/Dashboard";
import Login from "./pages/Login";
import Settings from "./pages/Settings";
import Service from "./pages/Service";
import Services from "./pages/Services";
import Service from "./pages/Service";
require("./assets/css/bootstrap.min.css")
require("./assets/css/base.css")
// require("./assets/js/bootstrap.min")
// require("./assets/js/flatpickr")
// require("./assets/js/inputTags.min")
// require("./assets/js/rangePlugin")
// require("./assets/js/sortable.min")
const routes = [
{
path: '/',
name: 'Index',
component: Index
},
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard
},
{
path: '/settings',
name: 'Settings',
component: Settings
},
{
path: '/',
name: 'Index',
component: Index
},
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard,
alias: ['/dashboard/settings', '/dashboard/services', '/dashboard/messages', '/dashboard/groups', '/dashboard/users', '/dashboard/logs', '/dashboard/help']
},
{
path: '/login',
name: 'Login',
component: Login
},
{ path: '/logout', redirect: '/' },
{
path: '/settings',
name: 'Settings',
component: Settings
},
{
path: '/services',
name: 'Services',
@ -34,12 +48,12 @@ const routes = [
{
path: '/service/:id',
name: 'Service',
component: Service
component: Service,
props: true
}
];
const router = new VueRouter
({
const router = new VueRouter({
mode: 'history',
routes
})

View File

@ -1,53 +1,85 @@
<template>
<div v-show="core" class="container col-md-7 col-sm-12 mt-2 sm-container">
<div>
<Login v-show="token === null"/>
<Header :core="core"/>
<div v-show="token !== null" class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div v-for="(group, index) in groups" v-bind:key="index">
<Group :group=group />
</div>
<TopNav :changeView="changeView"/>
<div class="col-12">
<MessageBlock/>
</div>
<DashboardIndex v-show="view === 'DashboardIndex'" :services="services"/>
<div class="col-12 full-col-12">
<DashboardServices v-show="view === 'DashboardServices'" :services="services"/>
<div v-for="(service, index) in services" v-bind:key="index">
<ServiceBlock :service=service />
</div>
<DashboardUsers v-show="view === 'DashboardUsers'" :services="services"/>
</div>
<DashboardMessages v-show="view === 'DashboardMessages'" :services="services"/>
<Settings v-show="view === 'Settings'" :services="services"/>
</div>
</div>
</template>
<script>
import ServiceBlock from '../components/Service/ServiceBlock.vue'
import MessageBlock from "../components/Index/MessageBlock";
import Group from "../components/Index/Group";
import Header from "../components/Index/Header";
import Api from "../components/API"
import Api from "../components/API"
import Login from "./Login";
import TopNav from "../components/Dashboard/TopNav";
import DashboardIndex from "../components/Dashboard/DashboardIndex";
import DashboardServices from "../components/Dashboard/DashboardServices";
import DashboardUsers from "../components/Dashboard/DashboardUsers";
import DashboardMessages from "../components/Dashboard/DashboardMessages";
import Settings from "./Settings";
export default {
export default {
name: 'Dashboard',
components: {
Header,
Group,
MessageBlock,
ServiceBlock,
Settings,
DashboardMessages,
DashboardUsers,
DashboardServices,
DashboardIndex,
TopNav,
Login,
},
data () {
return {
services: null,
groups: null,
core: null,
token: null,
view: "DashboardIndex",
}
},
beforeMount() {
created() {
this.pathView(this.$route.path)
this.token = Api.token()
this.loadAll()
},
methods: {
pathView (path) {
switch (path) {
case "/dashboard/settings":
this.view = "Settings"
break
case "/dashboard/users":
this.view = "DashboardUsers"
break
case "/dashboard/messages":
this.view = "DashboardMessages"
break
case "/dashboard/services":
this.view = "DashboardServices"
break
default:
this.view = "DashboardIndex"
}
},
changeView (v, name) {
this.view = v
this.$router.push('/'+name)
},
async loadAll () {
this.token = await Api.token()
this.core = await Api.root()
this.groups = await Api.groups()
this.services = await Api.services()

View File

@ -22,13 +22,13 @@
</template>
<script>
import ServiceBlock from '../components/Service/ServiceBlock.vue'
import MessageBlock from "../components/Index/MessageBlock";
import Group from "../components/Index/Group";
import Header from "../components/Index/Header";
import Api from "../components/API"
import ServiceBlock from '../components/Service/ServiceBlock.vue'
import MessageBlock from "../components/Index/MessageBlock";
import Group from "../components/Index/Group";
import Header from "../components/Index/Header";
import Api from "../components/API"
export default {
export default {
name: 'Index',
components: {
Header,
@ -41,13 +41,19 @@ export default {
services: null,
groups: null,
core: null,
auth: null
}
},
beforeMount() {
created() {
this.auth = Api.authToken()
this.core = Api.root()
},
mounted() {
this.loadAll()
},
methods: {
async loadAll () {
this.auth = Api.authToken()
this.core = await Api.root()
this.groups = await Api.groups()
this.services = await Api.services()

View File

@ -0,0 +1,61 @@
<template>
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="col-10 offset-1 col-md-8 offset-md-2 mt-md-2">
<div class="col-12 col-md-8 offset-md-2 mb-4">
<img class="col-12 mt-5 mt-md-0" :src="require(`@/assets/banner.png`)">
</div>
{{auth}}
<form id="login_form" @submit="login" method="post">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" v-model="username" name="username" class="form-control" id="username" placeholder="Username" autocorrect="off" autocapitalize="none">
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" v-model="password" name="password" class="form-control" id="password" placeholder="Password">
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block mb-3">Sign in</button>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
import Api from "../components/API"
export default {
name: 'Login',
components: {
},
data () {
return {
username: "",
password: "",
auth: null
}
},
methods: {
async login (e) {
e.preventDefault();
const auth = await Api.login(this.username, this.password)
if (auth.token !== null) {
this.auth = Api.saveToken(this.username, auth.token)
await this.$router.push('/dashboard')
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,16 +1,443 @@
<template>
<div class="container col-md-7 col-sm-12 mt-2 sm-container">
{{service}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<TopNav/>
<div class="col-12 mb-4">
<span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span>
<h4 class="mt-2"><a href="">{{service.name}}</a> - {{service.name}}
<span class="badge bg-success float-right d-none d-md-block">ONLINE</span>
</h4>
<div class="row stats_area mt-5 mb-5">
<div class="col-4">
<span class="lg_number">100%</span>
Online last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">31ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">85.70%</span>
Total Uptime
</div>
</div>
<div class="service-chart-container">
<div id="service"></div>
<div id="service-bar"></div>
</div>
<div class="service-chart-heatmap">
<div id="service_heatmap"></div>
</div>
<form id="service_date_form" class="col-12 mt-2 mb-3">
<input type="text" class="d-none" name="start" id="service_start" data-input>
<span data-toggle title="toggle" id="start_date" class="text-muted small float-left pointer mt-2">Thu, 09 Jan 2020 to Thu, 16 Jan 2020</span>
<button type="submit" class="btn btn-light btn-sm mt-2">Set Timeframe</button>
<input type="text" class="d-none" name="end" id="service_end" data-input>
<div id="start_container"></div>
<div id="end_container"></div>
</form>
<nav class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs" role="serviceLists">
<a class="flex-sm-fill text-sm-center nav-link active" id="edit-tab" data-toggle="tab" href="#edit" role="tab" aria-controls="edit" aria-selected="false">Edit Service</a>
<a class="flex-sm-fill text-sm-center nav-link" id="failures-tab" data-toggle="tab" href="#failures" role="tab" aria-controls="failures" aria-selected="true">Failures</a>
<a class="flex-sm-fill text-sm-center nav-link disabled" id="incidents-tab" data-toggle="tab" href="#incidents" role="tab" aria-controls="incidents" aria-selected="true">Incidents</a>
<a class="flex-sm-fill text-sm-center nav-link" id="checkins-tab" data-toggle="tab" href="#checkins" role="tab" aria-controls="checkins" aria-selected="false">Checkins</a>
<a class="flex-sm-fill text-sm-center nav-link" id="response-tab" data-toggle="tab" href="#response" role="tab" aria-controls="response" aria-selected="false">Response</a>
</nav>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade" id="failures" role="serviceLists" aria-labelledby="failures-tab">
<div class="list-group mt-3 mb-4">
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Incorrect HTTP Status Code</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">HTTP Status Code 502 did not match 200</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Failed</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">HTTP Error Get https://api.statping.com: dial tcp 162.248.92.36:443: connect: connection refused</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Failed</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">HTTP Error Get https://api.statping.com: dial tcp 162.248.92.36:443: connect: connection refused</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Failed</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">HTTP Error Get https://api.statping.com: dial tcp 162.248.92.36:443: connect: connection refused</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Failed</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">HTTP Error Get https://api.statping.com: dial tcp 162.248.92.36:443: connect: connection refused</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Failed</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">HTTP Error Get https://api.statping.com: dial tcp 162.248.92.36:443: connect: connection refused</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Reset</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">HTTP Error Get https://api.statping.com: read tcp 172.27.0.8:46586-&gt;162.248.92.36:443: read: connection reset by peer</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Failed</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">Could not get IP address for domain https://api.statping.com, lookup api.statping.com on 127.0.0.11:53: read udp 127.0.0.1:50890-&gt;127.0.0.11:53: read: connection refused</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Failed</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">Could not get IP address for domain https://api.statping.com, lookup api.statping.com on 127.0.0.11:53: read udp 127.0.0.1:35222-&gt;127.0.0.11:53: read: connection refused</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Failed</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">Could not get IP address for domain https://api.statping.com, lookup api.statping.com on 127.0.0.11:53: read udp 127.0.0.1:49817-&gt;127.0.0.11:53: read: connection refused</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Failed</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">Could not get IP address for domain https://api.statping.com, lookup api.statping.com on 127.0.0.11:53: read udp 127.0.0.1:57247-&gt;127.0.0.11:53: read: connection refused</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Reset</h5>
<small>4 weeks ago</small>
</div>
<p class="mb-1">HTTP Error Get https://api.statping.com: read tcp 172.27.0.10:52594-&gt;162.248.92.36:443: read: connection reset by peer</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Timed Out</h5>
<small>Last month</small>
</div>
<p class="mb-1">Could not get IP address for domain https://api.statping.com, lookup api.statping.com on 127.0.0.11:53: read udp 127.0.0.1:51238-&gt;127.0.0.11:53: i/o timeout</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Connection Timed Out</h5>
<small>Last month</small>
</div>
<p class="mb-1">Could not get IP address for domain https://api.statping.com, lookup api.statping.com on 127.0.0.11:53: read udp 127.0.0.1:47323-&gt;127.0.0.11:53: i/o timeout</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Incorrect HTTP Status Code</h5>
<small>Last month</small>
</div>
<p class="mb-1">HTTP Status Code 503 did not match 200</p>
</a>
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Incorrect HTTP Status Code</h5>
<small>Last month</small>
</div>
<p class="mb-1">HTTP Status Code 503 did not match 200</p>
</a>
</div>
</div>
<div class="tab-pane fade" id="incidents" role="serviceLists" aria-labelledby="incidents-tab">
</div>
<div class="tab-pane fade" id="checkins" role="serviceLists" aria-labelledby="checkins-tab">
<div class="card">
<div class="card-body">
<form class="ajax_form" action="api/checkin" data-redirect="/service/7" method="POST">
<div class="form-group row">
<div class="col-md-3">
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
<input type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
</div>
<div class="col-3">
<label for="checkin_interval" class="col-form-label">Interval (seconds)</label>
<input type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
</div>
<div class="col-3">
<label for="grace_period" class="col-form-label">Grace Period</label>
<input type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
</div>
<div class="col-3">
<label for="submit" class="col-form-label"></label>
<input type="hidden" name="service_id" class="form-control" id="service_id" value="7">
<button type="submit" id="submit" class="btn btn-success d-block" style="margin-top: 14px;">Save Checkin</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="tab-pane fade" id="response" role="serviceLists" aria-labelledby="response-tab">
<div class="col-12 mt-4">
<h3>Last Response</h3>
<textarea rows="8" class="form-control" readonly>invalid route</textarea>
<div class="form-group row mt-2">
<label for="last_status_code" class="col-sm-3 col-form-label">HTTP Status Code</label>
<div class="col-sm-2">
<input type="text" id="last_status_code" class="form-control" value="200" readonly>
</div>
</div>
</div>
</div>
<div class="tab-pane fade show active" id="edit" role="serviceLists" aria-labelledby="edit-tab">
<div class="card">
<div class="card-body">
<form class="ajax_form" action="api/services/7" data-redirect="services" method="POST">
<h4 class="mb-5 text-muted">Basic Information</h4>
<div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8">
<input type="text" name="name" class="form-control" id="service_name" value="Statping API" placeholder="Name" required spellcheck="false" autocorrect="off">
<small class="form-text text-muted">Give your service a name you can recognize</small>
</div>
</div>
<div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Service Type</label>
<div class="col-sm-8">
<select name="type" class="form-control" id="service_type" value="http" readonly>
<option value="http" selected>HTTP Service</option>
<option value="tcp" >TCP Service</option>
<option value="udp" >UDP Service</option>
<option value="icmp" >ICMP Ping</option>
</select>
<small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
</div>
</div>
<div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-sm-8">
<input type="text" name="domain" class="form-control" id="service_url" value="https://api.statping.com" placeholder="https://google.com" required autocapitalize="none" spellcheck="false">
<small class="form-text text-muted">Statping will attempt to connect to this URL</small>
</div>
</div>
<div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Group</label>
<div class="col-sm-8">
<select name="group_id" class="form-control" id="group_id">
<option value="0" >None</option>
<option value="1" >JSON Test Servers</option>
<option value="2" >Google Servers</option>
<option value="3" selected>Statping Servers</option>
</select>
<small class="form-text text-muted">Attach this service to a group</small>
</div>
</div>
<h4 class="mt-5 mb-5 text-muted">Request Details</h4>
<div class="form-group row">
<label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8">
<select name="method" class="form-control" id="service_check_type" value="GET">
<option value="GET" selected>GET</option>
<option value="POST" >POST</option>
<option value="DELETE" >DELETE</option>
<option value="PATCH" >PATCH</option>
<option value="PUT" >PUT</option>
</select>
<small class="form-text text-muted">A GET request will simply request the endpoint, you can also send data with POST.</small>
</div>
</div>
<div class="form-group row d-none">
<label for="post_data" class="col-sm-4 col-form-label">Optional Post Data (JSON)</label>
<div class="col-sm-8">
<textarea name="post_data" class="form-control" id="post_data" rows="3" autocapitalize="none" spellcheck="false" placeholder='{"data": { "method": "success", "id": 148923 } }'></textarea>
<small class="form-text text-muted">Insert a JSON string to send data to the endpoint.</small>
</div>
</div>
<div class="form-group row">
<label for="headers" class="col-sm-4 col-form-label">HTTP Headers</label>
<div class="col-sm-8">
<input name="headers" class="form-control" id="headers" autocapitalize="none" spellcheck="false" placeholder='Authorization=1010101,Content-Type=application/json' value="">
<small class="form-text text-muted">Comma delimited list of HTTP Headers (KEY=VALUE,KEY=VALUE)</small>
</div>
</div>
<div class="form-group row">
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8">
<textarea name="expected" class="form-control" id="service_response" rows="3" autocapitalize="none" spellcheck="false" placeholder='(method)": "((\\"|[success])*)"'></textarea>
<small class="form-text text-muted">You can use plain text or insert <a target="_blank" href="https://regex101.com/r/I5bbj9/1">Regex</a> to validate the response</small>
</div>
</div>
<div class="form-group row">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" value="200" placeholder="200" id="service_response_code">
<small class="form-text text-muted">A status code of 200 is success, or view all the <a target="_blank" href="https://www.restapitutorial.com/httpstatuscodes.html">HTTP Status Codes</a></small>
</div>
</div>
<div class="form-group row d-none">
<label for="port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8">
<input type="number" name="port" class="form-control" value="" id="service_port" placeholder="8080">
</div>
</div>
<h4 class="mt-5 mb-5 text-muted">Additional Options</h4>
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8">
<input type="number" name="check_interval" class="form-control" value="60" min="1" id="service_interval" required>
<small id="interval" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
</div>
</div>
<div class="form-group row">
<label for="service_timeout" class="col-sm-4 col-form-label">Timeout in Seconds</label>
<div class="col-sm-8">
<input type="number" name="timeout" class="form-control" value="15" placeholder="15" id="service_timeout" min="1">
<small class="form-text text-muted">If the endpoint does not respond within this time it will be considered to be offline</small>
</div>
</div>
<div class="form-group row">
<label for="post_data" class="col-sm-4 col-form-label">Permalink URL</label>
<div class="col-sm-8">
<input type="text" name="permalink" class="form-control" value="" id="permalink" autocapitalize="none" spellcheck="true" placeholder='awesome_service'>
<small class="form-text text-muted">Use text for the service URL rather than the service number.</small>
</div>
</div>
<div class="form-group row d-none">
<label for="order" class="col-sm-4 col-form-label">List Order</label>
<div class="col-sm-8">
<input type="number" name="order" class="form-control" min="0" value="0" id="order">
<small class="form-text text-muted">You can also drag and drop services to reorder on the Services tab.</small>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" >
<label for="switch-verify-ssl">Verify SSL Certificate for this service</label>
<input type="hidden" name="verify_ssl" id="switch-verify-ssl-value" value="false">
</span>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Notifications</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" checked>
<label for="switch-notifications">Allow notifications to be sent for this service</label>
<input type="hidden" name="allow_notifications" id="switch-notifications-value" value="true">
</span>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Visible</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="public-option" class="switch" id="switch-public" checked>
<label for="switch-public">Show service details to the public</label>
<input type="hidden" name="public" id="switch-public-value" value="true">
</span>
</div>
</div>
<div class="form-group row">
<div class="col-6">
<button type="submit" class="btn btn-success btn-block">Update Service</button>
</div>
<div class="col-6">
<a href="service/7/delete_failures" data-method="GET" data-redirect="/service/7" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a>
</div>
</div>
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Api from "../components/API"
import Api from "../components/API"
import TopNav from "../components/Dashboard/TopNav";
export default {
export default {
name: 'Service',
components: {
TopNav
},
data () {
return {
@ -18,13 +445,19 @@ export default {
service: null,
}
},
beforeMount() {
mounted() {
this.service = this.$route.params.service
this.id = this.$route.params.id
this.getService()
if (!this.service) {
this.getService(this.id)
}
},
beforeMount() {
},
methods: {
async getService() {
this.service = await Api.services()
async getService(id) {
this.service = await Api.service(id)
}
}
}

View File

@ -1,12 +1,13 @@
<template>
<div class="container col-md-7 col-sm-12 mt-2 sm-container">
<div v-show="core" class="container col-md-7 col-sm-12 mt-2 sm-container">
</div>
</template>
<script>
import Api from "../components/API"
export default {
export default {
name: 'Services',
components: {
@ -14,13 +15,21 @@ export default {
data () {
return {
services: null,
groups: null,
core: null,
auth: null
}
},
beforeMount() {
this.loadAll()
},
methods: {
async loadAll () {
this.auth = Api.authToken()
this.core = await Api.root()
this.groups = await Api.groups()
this.services = await Api.services()
}
}
}
</script>

File diff suppressed because it is too large Load Diff

View File

@ -6341,7 +6341,7 @@ querystring-es3@^0.2.0:
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
querystring@0.2.0:
querystring@0.2.0, querystring@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=

1
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/agnivade/levenshtein v1.0.2 // indirect
github.com/ararog/timeago v0.0.0-20160328174124-e9969cf18b8d
github.com/daaku/go.zipexe v1.0.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0 // indirect

2
go.sum
View File

@ -36,6 +36,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=

View File

@ -18,12 +18,14 @@ package handlers
import (
"bytes"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/utils"
"net/http"
"strconv"
"time"
)
func dashboardHandler(w http.ResponseWriter, r *http.Request) {
@ -36,19 +38,12 @@ func dashboardHandler(w http.ResponseWriter, r *http.Request) {
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
if sessionStore == nil {
resetCookies()
}
session, _ := sessionStore.Get(r, cookieKey)
form := parseForm(r)
username := form.Get("username")
password := form.Get("password")
user, auth := core.AuthUser(username, password)
if auth {
session.Values["authenticated"] = true
session.Values["user_id"] = user.Id
session.Values["admin"] = user.Admin.Bool
session.Save(r, w)
setJwtToken(user, w)
utils.Log.Infoln(fmt.Sprintf("User %v logged in from IP %v", user.Username, r.RemoteAddr))
http.Redirect(w, r, basePath+"dashboard", http.StatusSeeOther)
} else {
@ -58,11 +53,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
}
func logoutHandler(w http.ResponseWriter, r *http.Request) {
session, _ := sessionStore.Get(r, cookieKey)
session.Values["authenticated"] = false
session.Values["admin"] = false
session.Values["user_id"] = 0
session.Save(r, w)
removeJwtToken(w)
http.Redirect(w, r, basePath, http.StatusSeeOther)
}
@ -117,3 +108,63 @@ func exportHandler(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "export.json", utils.Now(), bytes.NewReader(export))
}
type JwtClaim struct {
Username string `json:"username"`
Admin bool `json:"admin"`
jwt.StandardClaims
}
func removeJwtToken(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: cookieKey,
Value: "",
Expires: time.Now().UTC(),
})
}
func setJwtToken(user *core.User, w http.ResponseWriter) (*JwtClaim, string) {
expirationTime := time.Now().Add(24 * time.Hour)
jwtClaim := &JwtClaim{
user.Username,
user.Admin.Bool,
jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
}}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtClaim)
tokenString, err := token.SignedString([]byte(jwtKey))
if err != nil {
utils.Log.Errorln("error setting token: ", err)
}
http.SetCookie(w, &http.Cookie{
Name: cookieKey,
Value: tokenString,
Expires: expirationTime,
})
return jwtClaim, tokenString
}
func apiLoginHandler(w http.ResponseWriter, r *http.Request) {
form := parseForm(r)
username := form.Get("username")
password := form.Get("password")
user, auth := core.AuthUser(username, password)
if auth {
utils.Log.Infoln(fmt.Sprintf("User %v logged in from IP %v", user.Username, r.RemoteAddr))
_, token := setJwtToken(user, w)
resp := struct {
Token string `json:"token"`
}{
token,
}
returnJson(resp, w, r)
} else {
resp := struct {
Error string `json:"error"`
}{
"incorrect authentication",
}
returnJson(resp, w, r)
}
}

View File

@ -20,6 +20,7 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go"
"html/template"
"net/http"
"os"
@ -28,7 +29,6 @@ import (
"strings"
"time"
"github.com/gorilla/sessions"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
@ -41,12 +41,12 @@ const (
)
var (
sessionStore *sessions.CookieStore
httpServer *http.Server
usingSSL bool
mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}`
templates = []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_integration.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
javascripts = []string{"charts.js", "chart_index.js"}
jwtKey string
httpServer *http.Server
usingSSL bool
mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}`
templates = []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_integration.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
javascripts = []string{"charts.js", "chart_index.js"}
)
// RunHTTPServer will start a HTTP server on a specific IP and port
@ -137,9 +137,6 @@ func IsFullAuthenticated(r *http.Request) bool {
if core.SetupMode {
return false
}
if sessionStore == nil {
return true
}
var token string
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
@ -152,19 +149,44 @@ func IsFullAuthenticated(r *http.Request) bool {
return IsAdmin(r)
}
func getJwtAuth(r *http.Request) (bool, string) {
c, err := r.Cookie(cookieKey)
if err != nil {
utils.Log.Errorln(err)
if err == http.ErrNoCookie {
return false, ""
}
return false, ""
}
tknStr := c.Value
var claims JwtClaim
tkn, err := jwt.ParseWithClaims(tknStr, &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(jwtKey), nil
})
if err != nil {
utils.Log.Errorln("error getting jwt token: ", err)
if err == jwt.ErrSignatureInvalid {
return false, ""
}
return false, ""
}
if !tkn.Valid {
utils.Log.Errorln("token is not valid")
return false, ""
}
return claims.Admin, claims.Username
}
// IsAdmin returns true if the user session is an administrator
func IsAdmin(r *http.Request) bool {
if core.SetupMode {
return false
}
session, err := sessionStore.Get(r, cookieKey)
if err != nil {
admin, username := getJwtAuth(r)
if username == "" {
return false
}
if session.Values["admin"] == nil {
return false
}
return session.Values["admin"].(bool)
return admin
}
// IsUser returns true if the user is registered
@ -175,14 +197,9 @@ func IsUser(r *http.Request) bool {
if os.Getenv("GO_ENV") == "test" {
return true
}
session, err := sessionStore.Get(r, cookieKey)
if err != nil {
return false
}
if session.Values["authenticated"] == nil {
return false
}
return session.Values["authenticated"].(bool)
ff, username := getJwtAuth(r)
fmt.Println(ff, username)
return username != ""
}
func loadTemplate(w http.ResponseWriter, r *http.Request) (*template.Template, error) {

View File

@ -19,7 +19,6 @@ import (
"fmt"
"github.com/99designs/gqlgen/handler"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/handlers/graphql"
"github.com/hunterlong/statping/source"
@ -115,6 +114,8 @@ func Router() *mux.Router {
// API Routes
r.Handle("/api", scopedRoute(apiIndexHandler))
r.Handle("/api/login", http.HandlerFunc(apiLoginHandler)).Methods("POST")
r.Handle("/api/logout", http.HandlerFunc(logoutHandler))
r.Handle("/api/renew", authenticated(apiRenewHandler, false))
r.Handle("/api/clear_cache", authenticated(apiClearCacheHandler, false))
@ -195,10 +196,5 @@ func resetRouter() {
}
func resetCookies() {
if core.CoreApp != nil {
cookie := fmt.Sprintf("%v_%v", core.CoreApp.ApiSecret, utils.Now().Nanosecond())
sessionStore = sessions.NewCookieStore([]byte(cookie))
} else {
sessionStore = sessions.NewCookieStore([]byte("secretinfo"))
}
jwtKey = fmt.Sprintf("%v_%v", core.CoreApp.ApiSecret, utils.Now().Nanosecond())
}