pull/429/head
Hunter Long 2020-01-28 04:15:48 -08:00
parent 7d927d2b24
commit c812715bb8
21 changed files with 272 additions and 110 deletions

View File

@ -96,7 +96,6 @@ func main() {
configs, err := core.LoadConfigFile(utils.Directory)
if err != nil {
log.Errorln(err)
core.SetupMode = true
writeAble, err := utils.DirWritable(utils.Directory)
if err != nil {
log.Fatalln(err)
@ -104,6 +103,7 @@ func main() {
if !writeAble {
log.Fatalf("Statping does not have write permissions at: %v\nYou can change this directory by setting the STATPING_DIR environment variable to a dedicated path before starting.", utils.Directory)
}
core.CoreApp.Setup = false
if err := handlers.RunHTTPServer(ipAddress, port); err != nil {
log.Fatalln(err)
}
@ -149,7 +149,7 @@ func mainProcess() error {
}
core.CoreApp.MigrateDatabase()
core.InitApp()
if !core.SetupMode {
if !core.CoreApp.Setup {
plugin.LoadPlugins()
if err := handlers.RunHTTPServer(ipAddress, port); err != nil {
log.Fatalln(err)

View File

@ -40,6 +40,7 @@ func LoadConfigFile(directory string) (*types.DbConfig, error) {
log.Debugln("attempting to read config file at: " + directory + "/config.yml")
file, err := ioutil.ReadFile(directory + "/config.yml")
if err != nil {
CoreApp.Setup = false
return nil, errors.New("config.yml file not found at " + directory + "/config.yml - starting in setup mode")
}
err = yaml.Unmarshal(file, &configs)

View File

@ -38,7 +38,6 @@ type Core struct {
var (
CoreApp *Core // CoreApp is a global variable that contains many elements
SetupMode bool // SetupMode will be true if Statping does not have a database connection
VERSION string // VERSION is set on build automatically by setting a -ldflag
log = utils.Log.WithField("type", "core")
)
@ -71,7 +70,7 @@ func InitApp() {
CoreApp.Notifications = notifier.AllCommunications
CoreApp.Integrations = integrations.Integrations
go DatabaseMaintence()
SetupMode = false
CoreApp.Setup = true
}
// InsertNotifierDB inject the Statping database instance to the Notifier package

View File

@ -1,12 +1,11 @@
<template>
<div id="app" v-if="loaded">
<router-view/>
<Footer version="DEV" />
<Footer version="DEV" v-if="$route.path !== '/setup'"/>
</div>
</template>
<script>
import Api from './components/API';
import Footer from "./components/Footer";
export default {
@ -19,40 +18,18 @@
loaded: false
}
},
async created () {
await this.setAllObjects()
async mounted() {
if (this.$route.path !== '/setup') {
const tk = JSON.parse(localStorage.getItem("statping_user"))
if (!this.$store.getters.hasPublicData) {
await this.$store.dispatch('loadAdmin')
}
}
this.loaded = true
this.$store.commit('setHasPublicData', true)
},
methods: {
async setAllObjects () {
await this.setCore()
await this.setServices()
await this.setGroups()
await this.setMessages()
await this.setToken()
this.$store.commit('setHasPublicData', true)
this.loaded = true
},
async setCore () {
const core = await Api.core()
this.$store.commit('setCore', core)
},
async setToken () {
const token = await Api.token()
this.$store.commit('setToken', token)
},
async setServices () {
const services = await Api.services()
this.$store.commit('setServices', services)
},
async setGroups () {
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
},
async setMessages () {
const messages = await Api.messages()
this.$store.commit('setMessages', messages)
}
}
}

View File

@ -134,7 +134,7 @@ HTML,BODY {
.chart-container {
position: relative;
height: 190px;
height: 200px;
width: 100%;
overflow: hidden;
}

View File

@ -16,6 +16,10 @@ class Api {
return axios.post('/api/core', obj).then(response => (response.data))
}
async setup_save (data) {
return axios.post('/api/setup', qs.stringify(data)).then(response => (response.data))
}
async services () {
return axios.get('/api/services').then(response => (response.data))
}

View File

@ -16,7 +16,7 @@
</div>
</div>
<div v-for="(service, index) in services" v-bind:key="index">
<div v-for="(service, index) in $store.getters.services" v-bind:key="index">
<ServiceInfo :service=service />
</div>
</div>
@ -32,7 +32,6 @@
},
data () {
return {
services: this.$store.getters.servicesInOrder()
}
},
methods: {

View File

@ -54,9 +54,6 @@
return {
}
},
created() {
},
methods: {
service (id) {

View File

@ -16,8 +16,8 @@
<th scope="col"></th>
</tr>
</thead>
<draggable tag="tbody" :list="services" :key="services.length" class="sortable" handle=".drag_icon">
<tr v-for="(service, index) in services" :key="index">
<draggable tag="tbody" :list="$store.getters.servicesInOrder" :key="this.$store.getters.servicesInOrder.length" class="sortable" handle=".drag_icon">
<tr v-for="(service, index) in $store.getters.services" :key="index">
<td>
<span class="drag_icon d-none d-md-inline">
<font-awesome-icon icon="bars" />
@ -65,7 +65,7 @@
</thead>
<draggable tag="tbody" v-model="groupsList" class="sortable_groups" handle=".drag_icon">
<tr v-for="(group, index) in $store.getters.cleanGroups()" v-bind:key="index">
<tr v-for="(group, index) in $store.getters.groupsCleaned" v-bind:key="index">
<td><span class="drag_icon d-none d-md-inline"><font-awesome-icon icon="bars" /></span> {{group.name}}</td>
<td>{{$store.getters.servicesInGroup(group.id).length}}</td>
<td>
@ -112,14 +112,9 @@
},
data () {
return {
services: []
}
},
async created() {
const services = await Api.services()
this.$store.commit('setServices', services)
this.services = this.$store.getters.servicesInOrder()
},
computed: {
servicesList: {
get() {

View File

@ -44,10 +44,6 @@
}
},
async created() {
const users = await Api.users()
this.$store.commit('setUsers', users)
},
methods: {
async deleteUser(u) {
let c = confirm(`Are you sure you want to delete user '${u.username}'?`)

View File

@ -0,0 +1,166 @@
<template>
<div class="container col-md-7 col-sm-12 mt-2 sm-container">
<div class="col-12 col-md-8 offset-md-2 mb-4">
<img class="col-12 mt-5 mt-md-0" src="/public/banner.png">
</div>
<div class="col-12">
<form @submit="">
<div class="row">
<div class="col-6">
<div class="form-group">
<label>Database Connection</label>
<select v-model="setup.db_connection" class="form-control">
<option value="sqlite">Sqlite</option>
<option value="postgres">Postgres</option>
<option value="mysql">MySQL</option>
</select>
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group" id="db_host">
<label>Host</label>
<input v-model="setup.db_host" type="text" class="form-control" placeholder="localhost">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group" id="db_port">
<label>Database Port</label>
<input v-model="setup.db_port" type="text" class="form-control" placeholder="localhost">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group" id="db_user">
<label>Username</label>
<input v-model="setup.db_user" type="text" class="form-control" placeholder="root">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group" id="db_password">
<label for="db_password">Password</label>
<input v-model="setup.db_password" type="password" class="form-control" placeholder="password123">
</div>
<div v-if="setup.db_connection !== 'sqlite'" class="form-group" id="db_database">
<label for="db_database">Database</label>
<input v-model="setup.db_database" type="text" class="form-control" placeholder="Database name">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label>Project Name</label>
<input v-model="setup.project" type="text" class="form-control" placeholder="Great Uptime" required>
</div>
<div class="form-group">
<label>Project Description</label>
<input v-model="setup.description" type="text" class="form-control" placeholder="Great Uptime">
</div>
<div class="form-group">
<label for="domain_input">Domain URL</label>
<input v-model="setup.domain" type="text" class="form-control" id="domain_input" required>
</div>
<div class="form-group">
<label>Admin Username</label>
<input v-model="setup.username" type="text" class="form-control" placeholder="admin" required>
</div>
<div class="form-group">
<label>Admin Password</label>
<input v-model="setup.password" type="password" class="form-control" placeholder="password" required>
</div>
<div class="form-group">
<label>Confirm Admin Password</label>
<input v-model="setup.confirm_password" type="password" class="form-control" placeholder="password" required>
</div>
<div class="form-group">
<span class="switch">
<input v-model="setup.sample_data" type="checkbox" class="switch" id="switch-normal">
<label for="switch-normal">Load Sample Data</label>
</span>
</div>
</div>
<div v-if="error" class="col-12 alert alert-danger">
{{error}}
</div>
<button @click="saveSetup" v-bind:disabled="loading" type="submit" class="btn btn-primary btn-block" :class="{'btn-primary': !loading, 'btn-default': loading}">
{{loading ? "Loading..." : "Save Settings"}}
</button>
</div>
</form>
</div>
</div>
</template>
<script>
import Api from "../components/API";
import Index from "../pages/Index";
export default {
name: 'Setup',
data () {
return {
error: null,
loading: false,
setup: {
db_connection: "sqlite",
db_host: "",
db_port: "",
db_user: "",
db_password: "",
db_database: "",
project: "",
description: "",
domain: "",
username: "",
password: "",
confirm_password: "",
sample_data: true
}
}
},
async created() {
const core = await Api.core()
if (core.setup) {
this.$router.push(Index)
}
},
mounted() {
this.setup.domain = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":"+window.location.port : "")
},
methods: {
async saveSetup(e) {
e.preventDefault();
this.loading = true
const s = this.setup
if (s.password !== s.confirm_password) {
alert('Passwords do not match!')
this.loading = false
return
}
const resp = await Api.setup_save(s)
if (resp.status === 'error') {
this.error = resp.error
this.loading = false
return
}
await this.completeAuth()
this.loading = false
this.$router.push('')
},
async completeAuth() {
const auth = await Api.login(this.setup.username, this.setup.password)
this.auth = Api.saveToken(this.setup.username, auth.token)
await this.$store.dispatch('loadAdmin')
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -6,31 +6,17 @@
</template>
<script>
import Login from "./Login";
import TopNav from "../components/Dashboard/TopNav";
import Api from "../components/API";
export default {
name: 'Dashboard',
components: {
TopNav,
Login,
},
data () {
return {
authenticated: false
}
},
mounted() {
if (this.$store.getters.token !== null) {
this.authenticated = true
}
},
methods: {
async setServices () {
const services = await Api.services()
this.$store.commit('setServices', services)
},
}
}
</script>

View File

@ -60,7 +60,7 @@
this.error = true
} else if (auth.token) {
this.auth = Api.saveToken(this.username, auth.token)
this.$store.commit('setToken', auth)
await this.$store.dispatch('loadRequired')
this.$router.push('/dashboard')
}
this.loading = false

View File

@ -50,7 +50,7 @@
<h2 class="mt-5">Additional Settings</h2>
<div class="row">
<div v-if="core.domain !== ''" class="row">
<div class="col-12">
<div class="row align-content-center">
<img class="rounded text-center" width="300" height="300" :src="qrcode">
@ -265,7 +265,8 @@
data () {
return {
tab: "v-pills-home-tab",
qrcode: ""
qrcode: "",
core: this.$store.getters.core
}
},
async created() {

View File

@ -9,13 +9,18 @@ import Settings from "./pages/Settings";
import Login from "./pages/Login";
import Service from "./pages/Service";
import VueRouter from "vue-router";
import Api from "./components/API";
import Setup from "./forms/Setup";
const routes = [
{
path: '/setup',
name: 'Setup',
component: Setup
},
{
path: '/',
name: 'Index',
component: Index
component: Index,
},
{
path: '/dashboard',

View File

@ -1,5 +1,6 @@
import Vuex from 'vuex'
import Vue from 'vue'
import Api from "./components/API"
Vue.use(Vuex)
@ -37,6 +38,9 @@ export default new Vuex.Store({
users: state => state.users,
notifiers: state => state.notifiers,
servicesInOrder: state => state.services,
groupsCleaned: state => state.groups.filter(g => g.name !== ''),
serviceById: (state) => (id) => {
return state.services.find(s => s.id === id)
},
@ -46,9 +50,6 @@ export default new Vuex.Store({
servicesInGroup: (state) => (id) => {
return state.services.filter(s => s.group_id === id)
},
servicesInOrder: (state) => () => {
return state.services
},
onlineServices: (state) => (online) => {
return state.services.filter(s => s.online === online)
},
@ -79,7 +80,7 @@ export default new Vuex.Store({
state.token = token
},
setServices(state, services) {
state.services = services.sort((a, b) => a.order_id - b.order_id)
state.services = services
},
setGroups(state, groups) {
state.groups = groups
@ -95,6 +96,23 @@ export default new Vuex.Store({
}
},
actions: {
async loadRequired(context) {
const core = await Api.core()
context.commit("setCore", core);
const services = await Api.services()
context.commit("setServices", services);
const groups = await Api.groups()
context.commit("setGroups", groups);
const messages = await Api.messages()
context.commit("setMessages", messages)
context.commit("setHasPublicData", true)
},
async loadAdmin(context) {
await context.dispatch('loadRequired')
const notifiers = await Api.notifiers()
context.commit("setNotifiers", notifiers);
const users = await Api.users()
context.commit("setUsers", users);
}
}
});

4
go.mod
View File

@ -18,9 +18,9 @@ require (
github.com/go-mail/mail v2.3.1+incompatible
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/gorilla/mux v1.7.3
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.1 // indirect
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/hunterlong/scopr v0.0.0
github.com/jinzhu/gorm v1.9.11
github.com/joho/godotenv v1.3.0
github.com/lib/pq v1.2.0 // indirect
@ -41,3 +41,5 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.2.7 // indirect
)
replace github.com/hunterlong/scopr v0.0.0 => ../scopr

4
go.sum
View File

@ -85,10 +85,6 @@ github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

View File

@ -103,7 +103,7 @@ func RunHTTPServer(ip string, port int) error {
// IsReadAuthenticated will allow Read Only authentication for some routes
func IsReadAuthenticated(r *http.Request) bool {
if core.SetupMode {
if !core.CoreApp.Setup {
return false
}
var token string
@ -132,7 +132,7 @@ func IsFullAuthenticated(r *http.Request) bool {
if core.CoreApp == nil {
return true
}
if core.SetupMode {
if !core.CoreApp.Setup {
return false
}
var token string
@ -185,7 +185,7 @@ func ScopeName(r *http.Request) string {
// IsAdmin returns true if the user session is an administrator
func IsAdmin(r *http.Request) bool {
if core.SetupMode {
if !core.CoreApp.Setup {
return false
}
if os.Getenv("GO_ENV") == "test" {
@ -201,7 +201,7 @@ func IsAdmin(r *http.Request) bool {
// IsUser returns true if the user is registered
func IsUser(r *http.Request) bool {
if core.SetupMode {
if !core.CoreApp.Setup {
return false
}
if os.Getenv("GO_ENV") == "test" {

View File

@ -16,11 +16,13 @@
package handlers
import (
"errors"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"net/http"
"os"
"strconv"
"time"
)
@ -39,11 +41,15 @@ func setupHandler(w http.ResponseWriter, r *http.Request) {
func processSetupHandler(w http.ResponseWriter, r *http.Request) {
var err error
if !core.SetupMode {
http.Redirect(w, r, basePath, http.StatusSeeOther)
if core.CoreApp.Setup {
sendErrorJson(errors.New("Statping has already been setup"), w, r)
return
}
if err = r.ParseForm(); err != nil {
log.Errorln(err)
sendErrorJson(err, w, r)
return
}
r.ParseForm()
dbHost := r.PostForm.Get("db_host")
dbUser := r.PostForm.Get("db_user")
dbPass := r.PostForm.Get("db_password")
@ -56,7 +62,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
description := r.PostForm.Get("description")
domain := r.PostForm.Get("domain")
email := r.PostForm.Get("email")
sample := r.PostForm.Get("sample_data") == "on"
sample, _ := strconv.ParseBool(r.PostForm.Get("sample_data"))
dir := utils.Directory
config := &types.DbConfig{
@ -80,34 +86,37 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
if _, err := core.CoreApp.SaveConfig(config); err != nil {
log.Errorln(err)
config.Error = err
setupResponseError(w, r, config)
sendErrorJson(err, w, r)
return
}
if _, err = core.LoadConfigFile(dir); err != nil {
log.Errorln(err)
config.Error = err
setupResponseError(w, r, config)
sendErrorJson(err, w, r)
return
}
if err = core.CoreApp.Connect(false, dir); err != nil {
log.Errorln(err)
core.DeleteConfig()
config.Error = err
setupResponseError(w, r, config)
sendErrorJson(err, w, r)
return
}
core.CoreApp.DropDatabase()
core.CoreApp.CreateDatabase()
if err = core.CoreApp.DropDatabase(); err != nil {
sendErrorJson(err, w, r)
return
}
if err = core.CoreApp.CreateDatabase(); err != nil {
sendErrorJson(err, w, r)
return
}
core.CoreApp, err = core.CoreApp.InsertCore(config)
if err != nil {
log.Errorln(err)
config.Error = err
setupResponseError(w, r, config)
sendErrorJson(err, w, r)
return
}
@ -120,13 +129,23 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
admin.Create()
if sample {
core.SampleData()
if err = core.SampleData(); err != nil {
sendErrorJson(err, w, r)
return
}
}
core.InitApp()
CacheStorage.Delete("/")
resetCookies()
time.Sleep(2 * time.Second)
http.Redirect(w, r, basePath, http.StatusSeeOther)
time.Sleep(1 * time.Second)
out := struct {
Message string `json:"message"`
Config *types.DbConfig `json:"config"`
}{
"okokok",
config,
}
returnJson(out, w, r)
}
func setupResponseError(w http.ResponseWriter, r *http.Request, a interface{}) {

View File

@ -38,6 +38,7 @@ type Core struct {
Footer NullString `gorm:"column:footer" json:"footer"`
Domain string `gorm:"not null;column:domain" json:"domain"`
Version string `gorm:"column:version" json:"version"`
Setup bool `gorm:"-" json:"setup"`
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`
UseCdn NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
UpdateNotify NullBool `gorm:"column:update_notify;default:true" json:"update_notify,omitempty"`