upgrades - plugins

pull/10/head
Hunter Long 2018-06-18 21:48:25 -07:00
parent 6bbf636bed
commit 8a6f6a2cf8
25 changed files with 400 additions and 400 deletions

View File

@ -17,7 +17,7 @@ services:
- mongodb
env:
- VERSION=0.151
- VERSION=0.16
matrix:
allow_failures:
@ -37,7 +37,6 @@ deploy:
- "build/statup-linux-x64"
- "build/statup-linux-x32"
- "build/statup-windows-x64.exe"
- "build/statup-linux-static"
skip_cleanup: true
notifications:

View File

@ -6,13 +6,13 @@ APP="statup"
rice embed-go
xgo --targets=darwin/amd64 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo --targets=darwin/386 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo -go 1.10.x --targets=darwin/amd64 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo -go 1.10.x --targets=darwin/386 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo --targets=linux/amd64 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo --targets=linux/386 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo -go 1.10.x --targets=linux/amd64 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo -go 1.10.x --targets=linux/386 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo --targets=windows/amd64 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo -go 1.10.x --targets=windows/amd64 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
cd build
ls

View File

@ -11,6 +11,8 @@ type Core struct {
Config string `db:"config"`
ApiKey string `db:"api_key"`
ApiSecret string `db:"api_secret"`
Style string `db:"style"`
Footer string `db:"footer"`
Version string `db:"version"`
Plugins []plugin.Info
Repos []PluginJSON

View File

@ -2,8 +2,8 @@ package main
import (
"fmt"
"github.com/hunterlong/statup/plugin"
"strings"
"time"
"upper.io/db.v3/lib/sqlbuilder"
"upper.io/db.v3/mysql"
"upper.io/db.v3/postgresql"
@ -29,6 +29,9 @@ func DbConnection(dbType string) error {
return err
}
} else if dbType == "mysql" {
if configs.Port == "" {
configs.Port = "3306"
}
mysqlSettings = mysql.ConnectionURL{
Database: configs.Database,
Host: configs.Host,
@ -40,9 +43,13 @@ func DbConnection(dbType string) error {
return err
}
} else {
if configs.Port == "" {
configs.Port = "5432"
}
host := fmt.Sprintf("%v:%v", configs.Host, configs.Port)
postgresSettings = postgresql.ConnectionURL{
Database: configs.Database,
Host: configs.Host,
Host: host,
User: configs.User,
Password: configs.Password,
}
@ -53,28 +60,10 @@ func DbConnection(dbType string) error {
}
//dbSession.SetLogging(true)
dbServer = dbType
plugin.SetDatabase(dbSession)
OnLoad(dbSession)
return err
}
func UpgradeDatabase() {
fmt.Println("New Version: ", core.Version)
fmt.Println("Current Version: ", VERSION)
if VERSION == core.Version {
fmt.Println("Database already up to date")
return
}
fmt.Println("Upgrading Database...")
upgrade, _ := sqlBox.String("upgrade.sql")
requests := strings.Split(upgrade, ";")
for _, request := range requests {
_, err := db.Exec(request)
if err != nil {
fmt.Println(err)
}
}
}
func DropDatabase() {
fmt.Println("Dropping Tables...")
down, _ := sqlBox.String("down.sql")
@ -124,22 +113,17 @@ func LoadSampleData() error {
Type: "https",
Method: "GET",
}
admin := &User{
Username: "admin",
Password: "admin",
Email: "admin@admin.com",
}
s1.Create()
s2.Create()
s3.Create()
s4.Create()
admin.Create()
for i := 0; i < 20; i++ {
for i := 0; i < 100; i++ {
s1.Check()
s2.Check()
s3.Check()
s4.Check()
time.Sleep(250 * time.Millisecond)
}
return nil
@ -147,7 +131,6 @@ func LoadSampleData() error {
func CreateDatabase() {
fmt.Println("Creating Tables...")
VERSION = "1.1.1"
sql := "postgres_up.sql"
if dbServer == "mysql" {
sql = "mysql_up.sql"

View File

@ -1,16 +1,56 @@
package main
import "github.com/hunterlong/statup/plugin"
import (
"github.com/fatih/structs"
"github.com/hunterlong/statup/plugin"
"upper.io/db.v3/lib/sqlbuilder"
)
func OnLoad(db sqlbuilder.Database) {
for _, p := range allPlugins {
p.OnLoad(db)
}
}
func OnSuccess(s *Service) {
for _, p := range allPlugins {
p.OnSuccess(s.ToP())
p.OnSuccess(structs.Map(s))
}
}
func OnFailure(s *Service) {
for _, p := range allPlugins {
p.OnFailure(s.ToP())
p.OnFailure(structs.Map(s))
}
}
func OnSettingsSaved(c *Core) {
for _, p := range allPlugins {
p.OnSettingsSaved(structs.Map(c))
}
}
func OnNewUser(u *User) {
for _, p := range allPlugins {
p.OnNewUser(structs.Map(u))
}
}
func OnNewService(s *Service) {
for _, p := range allPlugins {
p.OnNewService(structs.Map(s))
}
}
func OnDeletedService(s *Service) {
for _, p := range allPlugins {
p.OnDeletedService(structs.Map(s))
}
}
func OnUpdateService(s *Service) {
for _, p := range allPlugins {
p.OnUpdatedService(structs.Map(s))
}
}
@ -22,40 +62,3 @@ func SelectPlugin(name string) plugin.PluginActions {
}
return plugin.PluginInfo{}
}
func (s *Service) PluginFailures() []*plugin.Failure {
var failed []*plugin.Failure
for _, f := range s.Failures {
fail := &plugin.Failure{
f.Id,
f.Issue,
f.Service,
f.CreatedAt,
f.Ago,
}
failed = append(failed, fail)
}
return failed
}
func (s *Service) ToP() *plugin.Service {
out := &plugin.Service{
s.Id,
s.Name,
s.Domain,
s.Expected,
s.ExpectedStatus,
s.Interval,
s.Method,
s.Port,
s.CreatedAt,
s.Data,
s.Online,
s.Latency,
s.Online24Hours,
s.AvgResponse,
s.TotalUptime,
s.PluginFailures(),
}
return out
}

View File

@ -29,16 +29,21 @@ func (s *Service) CreateFailure(data FailureData) (int64, error) {
func (s *Service) SelectAllFailures() ([]*Failure, error) {
var fails []*Failure
col := dbSession.Collection("failures").Find("session", s.Id)
col := dbSession.Collection("failures").Find("service", s.Id)
err := col.All(&fails)
return fails, err
}
func (s *Service) LimitedFailures() ([]*Failure, error) {
func (s *Service) LimitedFailures() []*Failure {
var fails []*Failure
col := dbSession.Collection("failures").Find("session", s.Id).Limit(10)
err := col.All(&fails)
return fails, err
col := dbSession.Collection("failures").Find("service", s.Id).Limit(10)
col.All(&fails)
return fails
}
func (f *Failure) Delete() error {
col := dbSession.Collection("failures").Find("id", f.Id)
return col.Delete()
}
func CountFailures() (uint64, error) {

View File

@ -7,25 +7,54 @@
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/base.css">
<title>Statup | Dashboard</title>
<title>Statup | Help</title>
</head>
<body>
<div class="container">
{{template "nav"}}
{{if Auth}}
{{template "nav"}}
{{end}}
<div class="row">
<div class="col-12">
<h3>Help</h3>
<h2>Statup v{{ VERSION }} Help</h2>
Statup is an easy to use Status Page monitor for your websites and applications. Statup is developed in Go Language and you are able to create custom plugins with it!
<p>
<a href="https://github.com/hunterlong/statup"><img src="https://img.shields.io/github/stars/hunterlong/statup.svg?style=social&label=Stars"></a>
<a href="https://github.com/hunterlong/statup"><img src="https://img.shields.io/docker/build/hunterlong/statup.svg"></a>
<a href="https://github.com/hunterlong/statup"><img src="https://img.shields.io/github/release/hunterlong/statup.svg"></a>
</p>
<h2 class="mt-3">Services</h2>
For each website and application you want to add a new Service. Each Service will require a URL endpoint to test your applications status.
You can also add expected HTTP responses (regex allow), expected HTTP response codes, and other fields to make sure your service is online or offline.
<h2 class="mt-3">Users</h2>
Users can access the Statup Dashboard to add, remove, and view services.
<h2 class="mt-3">Plugins</h2>
Creating a plugin for Statup is not that difficult, if you know a little bit of Go Language you can create any type of application to be embedded into the Status framework.
Checkout the example plugin that includes all the interfaces, information, and custom HTTP routing at <a href="https://github.com/hunterlong/statup_plugin">https://github.com/hunterlong/statup_plugin</a>.
Anytime there is an action on your status page, all of your plugins will be notified of the change with the values that were changed or created.
<p></p>
Using the statup/plugin Golang package you can quickly implement the event listeners. Statup uses <a href="https://github.com/upper/db">upper.io/db.v3</a> for the database connection.
You can use the database inside of your plugin to create, update, and destroy tables/data. <b>Please only use respectable plugins!</b>
<h2 class="mt-3">Custom Stlying</h2>
On Statup Status Page server can you create your own custom stylesheet to be rendered on the index view of your status page. Go to <a href="/settings">Settings</a> and click on Custom Styling.
</div>
</div>
</div>
<script src="/js/jquery-3.3.1.slim.min.js"></script>
</body>

View File

@ -8,15 +8,20 @@
<link rel="stylesheet" href="/css/base.css">
<script src="/js/Chart.bundle.min.js"></script>
<title>{{.Project}} Status</title>
<title>{{.Core.Name}} Status</title>
</head>
<body>
<h1 class="text-center mb-4 mt-sm-3">{{.Project}}</h1>
<h1 class="text-center mb-4 mt-sm-3">{{.Core.Name}}</h1>
{{ if .Core.Description }}
<h5 class="text-center mb-1 mt-sm-2">{{ .Core.Description }}</h5>
{{ end }}
<div class="container">
<div class="col-12 mb-5">
<div class="list-group online_list">
@ -72,7 +77,7 @@
<canvas id="service_{{ .Id }}" width="400" height="120"></canvas>
{{ range .Failures }}
{{ range .LimitedFailures }}
<blockquote class="blockquote text-right mt-3">
<p class="mb-0">{{.ParseError}}</p>
<footer class="blockquote-footer">Reported <cite title="Source Title">{{.Ago}}</cite></footer>
@ -89,7 +94,10 @@
</div>
<div class="footer text-center">
<a href="https://statup.io" target="_blank">Created with Statup.io</a> | <a href="/dashboard">Dashboard</a>
{{ if .Core.Footer }}
{{ safe .Core.Footer }}
{{ end }}
<a href="https://statup.io" target="_blank">Statup made with ❤️</a> | <a href="/dashboard">Dashboard</a>
</div>
<script>
@ -151,6 +159,14 @@ var chartdata = new Chart(ctx, {
});
{{ end }}
</script>
{{ if .Core.Style }}
<style>
{{ safe .Core.Style }}
</style>
{{ end }}
<script src="/js/jquery-3.3.1.slim.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
</body>

View File

@ -23,7 +23,8 @@
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<a class="nav-link active" id="v-pills-home-tab" data-toggle="pill" href="#v-pills-home" role="tab" aria-controls="v-pills-home" aria-selected="true">Settings</a>
<a class="nav-link" id="v-pills-browse-tab" data-toggle="pill" href="#v-pills-browse" role="tab" aria-controls="v-pills-home" aria-selected="true">Browse Plugins</a>
<a class="nav-link" id="v-pills-home-tab" data-toggle="pill" href="#v-pills-style" role="tab" aria-controls="v-pills-style" aria-selected="false">Styling</a>
<a class="nav-link" id="v-pills-browse-tab" data-toggle="pill" href="#v-pills-browse" role="tab" aria-controls="v-pills-home" aria-selected="false">Browse Plugins</a>
{{ range .Plugins }}
<a class="nav-link text-capitalize" id="v-pills-{{.Name}}-tab" data-toggle="pill" href="#v-pills-{{.Name}}" role="tab" aria-controls="v-pills-profile" aria-selected="false">{{.Name}}</a>
{{end}}
@ -46,12 +47,31 @@
<input type="text" name="description" class="form-control" value="{{ .Description }}" id="formGroupExampleInput" placeholder="Great Uptime">
</div>
<div class="form-group">
<label for="formGroupExampleInput">Custom Footer</label>
<textarea rows="4" name="footer" class="form-control" id="formGroupExampleInput">{{ .Footer }}</textarea>
</div>
<button type="submit" class="btn btn-primary btn-block">Save Settings</button>
</form>
</div>
<div class="tab-pane fade" id="v-pills-style" role="tabpanel" aria-labelledby="v-pills-style-tab">
<h3>Custom Style</h3>
<form method="POST" action="/settings">
<div class="form-group">
<textarea rows="15" name="style" class="form-control" id="formGroupExampleInput">{{ .Style }}</textarea>
</div>
<button type="submit" class="btn btn-primary btn-block">Save Settings</button>
</form>
</div>
<div class="tab-pane fade" id="v-pills-browse" role="tabpanel" aria-labelledby="v-pills-browse-tab">
{{ range .Repos }}
<div class="card col-6" style="width: 18rem;">
@ -98,5 +118,6 @@
<script src="/js/jquery-3.3.1.slim.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>

View File

@ -12,10 +12,9 @@
</head>
<body>
<div class="container">
{{if .Auth}}
{{if Auth}}
{{template "nav"}}
{{end}}
<div class="row">
@ -24,8 +23,8 @@
<div class="col-12 mb-4">
<h3 class="mt-2">{{ .Service.Name }}
{{if .Service.Online}}
<h3 class="mt-2">{{ .Name }}
{{if .Online }}
<span class="badge online_badge float-right">ONLINE</span>
{{ else }}
<span class="badge offline_badge float-right">OFFLINE</span>
@ -34,24 +33,24 @@
<div class="row stats_area mt-5 mb-5">
<div class="col-4">
<span class="lg_number">{{.Service.Online24}}%</span>
<span class="lg_number">{{.Online24}}%</span>
Online last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{.Service.AvgTime}}ms</span>
<span class="lg_number">{{.AvgTime}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{.Service.AvgUptime}}%</span>
<span class="lg_number">{{.AvgUptime}}%</span>
Total Uptime
</div>
</div>
<canvas id="service" width="400" height="120"></canvas>
{{ range .Service.Failures }}
{{ range .Failures }}
<blockquote class="blockquote text-right mt-3">
<p class="mb-0">{{.ParseError}}</p>
<footer class="blockquote-footer">Reported <cite title="Source Title">{{.Ago}}</cite></footer>
@ -63,52 +62,53 @@
</div>
{{if .Auth}}
{{if Auth}}
<div class="col-12">
<h3>Edit Service</h3>
<form action="/service/{{.Service.Id}}" method="POST">
<form action="/service/{{.Id}}" method="POST">
<div class="form-group row">
<label for="inputEmail3" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8">
<input type="text" name="name" class="form-control" value="{{.Service.Name}}" id="inputEmail3">
<input type="text" name="name" class="form-control" value="{{.Name}}" id="inputEmail3">
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-sm-8">
<input type="text" name="domain" class="form-control" value="{{.Service.Domain}}" id="inputPassword3">
<input type="text" name="domain" class="form-control" value="{{.Domain}}" id="inputPassword3">
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8">
<input type="text" name="expected" class="form-control" value="{{.Service.Expected}}" id="inputPassword3">
<input type="text" name="expected" class="form-control" value="{{.Expected}}" id="inputPassword3">
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" 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="{{.Service.ExpectedStatus}}" id="inputPassword3">
<input type="number" name="expected_status" class="form-control" value="{{.ExpectedStatus}}" id="inputPassword3">
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">HTTP Method</label>
<div class="col-sm-8">
<input type="text" name="method" class="form-control" value="{{.Service.Method}}" id="inputPassword3">
<input type="text" name="method" class="form-control" value="{{.Method}}" id="inputPassword3">
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8">
<input type="number" name="interval" class="form-control" value="{{.Service.Interval}}" id="inputPassword3">
<input type="number" name="interval" class="form-control" value="{{.Interval}}" id="inputPassword3">
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<a class="btn btn-primary">Test</a>
<button type="submit" class="btn btn-success">Update Service</button>
<a href="/service/{{ .Id }}/delete_failures" class="btn btn-danger">Delete All Failures</a>
</div>
</div>
</form>
@ -117,6 +117,13 @@
{{end}}
{{ range .Failures }}
<blockquote class="blockquote text-right mt-3">
<p class="mb-0">{{.ParseError}}</p>
<footer class="blockquote-footer">Reported <cite title="Source Title">{{.Ago}}</cite></footer>
</blockquote>
{{ end }}
</div>
</div>
@ -129,7 +136,7 @@
data: {
datasets: [{
label: 'Response Time (Milliseconds)',
data: {{js .Service.GraphData}},
data: {{js .GraphData}},
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',

View File

@ -38,7 +38,7 @@
<td>{{.Name}}</td>
<td>{{if .Online}}<span class="badge badge-success">ONLINE</span>{{else}}<span class="badge badge-danger">OFFLINE</span>{{end}} </td>
<td class="text-right">
<div class="btn-group" data-toggle="buttons">
<div class="btn-group">
<a href="/service/{{.Id}}" class="btn btn-primary">View</a>
<a href="/service/{{.Id}}/delete" class="btn btn-danger">Delete</a>
</div>

View File

@ -15,6 +15,12 @@
<div class="container">
{{ if .Error }}
<div class="alert alert-danger" role="alert">
{{ .Error }}
</div>
{{ end }}
<form method="POST" action="/setup">
<div class="row">
@ -30,23 +36,23 @@
</div>
<div class="form-group" id="db_host">
<label for="formGroupExampleInput">Host</label>
<input type="text" name="db_host" class="form-control" value="localhost" placeholder="localhost">
<input type="text" name="db_host" class="form-control" value="{{.DbHost}}" placeholder="localhost">
</div>
<div class="form-group" id="db_port">
<label for="formGroupExampleInput">Database Port</label>
<input type="text" name="db_port" class="form-control" value="5555" placeholder="localhost">
<input type="text" name="db_port" class="form-control" value="{{.DbPort}}" placeholder="localhost">
</div>
<div class="form-group" id="db_user">
<label for="formGroupExampleInput2">Username</label>
<input type="text" name="db_user" class="form-control" value="root" placeholder="root">
<input type="text" name="db_user" class="form-control" value="{{.DbUser}}" placeholder="root">
</div>
<div class="form-group" id="db_password">
<label for="formGroupExampleInput2">Password</label>
<input type="password" name="db_password" class="form-control" id="formGroupExampleInput2" value="223352hunter" placeholder="password123">
<input type="password" name="db_password" class="form-control" value="{{.DbPass}}" id="formGroupExampleInput2" value="" placeholder="password123">
</div>
<div class="form-group" id="db_database">
<label for="formGroupExampleInput2">Database</label>
<input type="text" name="db_database" class="form-control" id="formGroupExampleInput2" value="uptime" placeholder="uptimeapi">
<input type="text" name="db_database" class="form-control" value="{{.DbData}}" id="formGroupExampleInput2" value="statup" placeholder="Database name">
</div>
</div>
@ -55,27 +61,27 @@
<div class="form-group">
<label for="formGroupExampleInput">Project Name</label>
<input type="text" name="project" class="form-control" id="formGroupExampleInput" placeholder="Great Uptime">
<input type="text" name="project" class="form-control" value="{{.Project}}" id="formGroupExampleInput" placeholder="Great Uptime">
</div>
<div class="form-group">
<label for="formGroupExampleInput">Project Description</label>
<input type="text" name="description" class="form-control" id="formGroupExampleInput" placeholder="Great Uptime">
<input type="text" name="description" class="form-control" value="{{.Description}}" id="formGroupExampleInput" placeholder="Great Uptime">
</div>
<div class="form-group">
<label for="formGroupExampleInput">Admin Username</label>
<input type="text" name="username" class="form-control" id="formGroupExampleInput" value="admin" placeholder="admin">
<input type="text" name="username" class="form-control" value="{{.Username}}" id="formGroupExampleInput" value="admin" placeholder="admin">
</div>
<div class="form-group">
<label for="formGroupExampleInput">Admin Password</label>
<input type="password" name="password" class="form-control" id="formGroupExampleInput" value="admin" placeholder="admin">
<input type="password" name="password" class="form-control" value="{{.Password}}" id="formGroupExampleInput" value="admin" placeholder="admin">
</div>
<div class="form-group">
<label for="formGroupExampleInput">Confirm Password</label>
<input type="password" name="confirm_password" class="form-control" id="formGroupExampleInput" value="admin" placeholder="admin">
<input type="password" name="confirm_password" class="form-control" value="{{.Password}}" id="formGroupExampleInput" value="admin" placeholder="admin">
</div>
<div class="form-group">

View File

@ -27,6 +27,7 @@
<tr>
<th scope="col">#</th>
<th scope="col">Username</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
@ -34,6 +35,11 @@
<tr>
<th scope="row">{{.Id}}</th>
<td>{{.Username}}</td>
<td class="text-right">
<div class="btn-group">
<a href="/users/{{.Id}}/delete" class="btn btn-danger">Delete</a>
</div>
</td>
</tr>
{{end}}
</tbody>

119
main.go
View File

@ -1,14 +1,12 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
"github.com/GeertJohan/go.rice"
"github.com/go-yaml/yaml"
"github.com/gorilla/sessions"
"github.com/hunterlong/statup/plugin"
_ "github.com/lib/pq"
"golang.org/x/crypto/bcrypt"
"io"
"io/ioutil"
@ -20,7 +18,6 @@ import (
)
var (
db *sql.DB
configs *Config
core *Core
store *sessions.CookieStore
@ -93,114 +90,6 @@ func DownloadFile(filepath string, url string) error {
return nil
}
func SelectSettings(p plugin.Info) map[string]string {
data := make(map[string]string)
var tableInput []string
for _, v := range p.Form {
val := fmt.Sprintf("%v", v.InputName)
tableInput = append(tableInput, val)
}
ins := strings.Join(tableInput, ", ")
sql := fmt.Sprintf("SELECT %v FROM settings_%v LIMIT 1", ins, p.Name)
rows, err := db.Query(sql)
if err != nil {
fmt.Println("SQL ERROR: ", err)
return map[string]string{}
}
count := len(p.Form)
valuePtrs := make([]interface{}, count)
values := make([]interface{}, count)
for rows.Next() {
for i, _ := range p.Form {
valuePtrs[i] = &values[i]
}
err = rows.Scan(valuePtrs...)
if err != nil {
panic(err)
}
for i, col := range p.Form {
val := values[i]
b, _ := val.([]byte)
realVal := string(b)
//col.ChangeVal(realVal)
fmt.Println(col.Value, realVal)
data[col.InputName] = realVal
}
}
return data
}
func CreateSettingsTable(p plugin.Info) string {
var tableValues []string
tableValues = append(tableValues, "plugin text")
tableValues = append(tableValues, "enabled bool")
for _, v := range p.Form {
tb := fmt.Sprintf("%v %v", v.InputName, v.InputType)
tableValues = append(tableValues, tb)
}
vals := strings.Join(tableValues, ", ")
out := fmt.Sprintf("CREATE TABLE settings_%v (%v);", p.Name, vals)
smtp, _ := db.Prepare(out)
_, _ = smtp.Exec()
InitalSettings(p)
return out
}
func InitalSettings(p plugin.Info) {
var tableValues []string
var tableInput []string
tableValues = append(tableValues, "plugin")
tableInput = append(tableInput, fmt.Sprintf("'%v'", p.Name))
tableValues = append(tableValues, "enabled")
tableInput = append(tableInput, "false")
for _, v := range p.Form {
val := fmt.Sprintf("'%v'", v.Value)
tableValues = append(tableValues, v.InputName)
tableInput = append(tableInput, val)
}
vals := strings.Join(tableValues, ",")
ins := strings.Join(tableInput, ",")
sql := fmt.Sprintf("INSERT INTO settings_%v(%v) VALUES(%v);", p.Name, vals, ins)
smtp, _ := db.Prepare(sql)
_, _ = smtp.Exec()
}
func UpdateSettings(p plugin.Info, data map[string]string) {
var tableInput []string
for _, v := range p.Form {
newValue := data[v.InputName]
val := fmt.Sprintf("%v='%v'", v.InputName, newValue)
tableInput = append(tableInput, val)
}
ins := strings.Join(tableInput, ", ")
sql := fmt.Sprintf("UPDATE settings_%v SET %v WHERE plugin='%v';", p.Name, ins, p.Name)
smtp, _ := db.Prepare(sql)
_, _ = smtp.Exec()
}
//func DownloadPlugin(name string) {
// plugin := SelectPlugin(name)
// var _, err = os.Stat("plugins/" + plugin.Namespace)
@ -234,7 +123,6 @@ func UpdateSettings(p plugin.Info, data map[string]string) {
func main() {
var err error
VERSION = "1.1.1"
fmt.Printf("Starting Statup v%v\n", VERSION)
RenderBoxes()
configs, err = LoadConfig()
@ -270,8 +158,7 @@ func mainProcess() {
}
func throw(err error) {
panic(err)
fmt.Println(err)
fmt.Println("ERROR: ", err)
os.Exit(1)
}
@ -317,12 +204,12 @@ func LoadPlugins() {
continue
}
plugActions.OnLoad()
allPlugins = append(allPlugins, plugActions)
core.Plugins = append(core.Plugins, plugActions.GetInfo())
}
OnLoad(dbSession)
fmt.Printf("Loaded %v Plugins\n", len(allPlugins))
ForEachPlugin()

View File

@ -8,7 +8,6 @@ import (
)
func TestInit(t *testing.T) {
VERSION = "1.1.1"
RenderBoxes()
os.Remove("./statup.db")
Router()
@ -26,6 +25,7 @@ func TestMySQLMakeConfig(t *testing.T) {
"This is a test of Statup.io!",
"admin",
"admin",
nil,
}
err := config.Save()
assert.Nil(t, err)
@ -64,6 +64,7 @@ func TestSqliteMakeConfig(t *testing.T) {
"This is a test of Statup.io!",
"admin",
"admin",
nil,
}
err := config.Save()
assert.Nil(t, err)
@ -93,6 +94,7 @@ func TestPostgresMakeConfig(t *testing.T) {
"This is a test of Statup.io!",
"admin",
"admin",
nil,
}
err := config.Save()
assert.Nil(t, err)

View File

@ -3,7 +3,6 @@ package plugin
import (
"fmt"
"net/http"
"time"
"upper.io/db.v3/lib/sqlbuilder"
)
@ -14,6 +13,10 @@ import (
//
// https://statup.io
//
//
// An expandable plugin framework that will still
// work even if there's an update or addition.
//
var (
DB sqlbuilder.Database
@ -34,56 +37,22 @@ type PluginInfo struct {
type PluginActions interface {
GetInfo() Info
SetInfo(map[string]string) Info
SetInfo(map[string]interface{}) Info
Routes() []Routing
OnSave(map[string]string)
OnFailure(*Service)
OnSuccess(*Service)
OnSettingsSaved(map[string]string)
OnNewUser(*User)
OnNewService(*Service)
OnUpdatedService(*Service)
OnDeletedService(*Service)
OnInstall()
OnUninstall()
OnSave(map[string]interface{})
OnFailure(map[string]interface{})
OnSuccess(map[string]interface{})
OnSettingsSaved(map[string]interface{})
OnNewUser(map[string]interface{})
OnNewService(map[string]interface{})
OnUpdatedService(map[string]interface{})
OnDeletedService(map[string]interface{})
OnInstall(map[string]interface{})
OnUninstall(map[string]interface{})
OnBeforeRequest(map[string]interface{})
OnAfterRequest(map[string]interface{})
OnShutdown()
OnLoad()
OnBeforeRequest()
OnAfterRequest()
}
type User struct {
Id int64
Username string
Password string
Email string
}
type Service struct {
Id int64
Name string
Domain string
Expected string
ExpectedStatus int
Interval int
Method string
Port int
CreatedAt time.Time
Data string
Online bool
Latency float64
Online24Hours float32
AvgResponse string
TotalUptime string
Failures []*Failure
}
type Failure struct {
Id int
Issue string
Service int64
CreatedAt time.Time
Ago string
OnLoad(sqlbuilder.Database)
}
type Routing struct {
@ -95,13 +64,4 @@ type Routing struct {
type Info struct {
Name string
Description string
Form []*FormElement
}
type FormElement struct {
Name string
Description string
InputName string
InputType string
Value string
}

View File

@ -1,7 +0,0 @@
package plugin
import "testing"
func TestInit(t *testing.T) {
}

Binary file not shown.

View File

@ -4,7 +4,6 @@ import (
"crypto/sha1"
"encoding/json"
"fmt"
"github.com/hunterlong/statup/plugin"
"math/rand"
"strconv"
"time"
@ -31,11 +30,10 @@ type Service struct {
AvgResponse string `json:"avg_response"`
TotalUptime string `json:"uptime"`
Failures []*Failure `json:"failures"`
plugin.Service
}
func SelectService(id int64) (Service, error) {
var service Service
func SelectService(id int64) (*Service, error) {
var service *Service
col := dbSession.Collection("services")
res := col.Find("id", id)
err := res.One(&service)
@ -107,8 +105,7 @@ func (s *Service) GraphData() string {
d = append(d, o)
}
data, _ := json.Marshal(d)
s.Data = string(data)
return s.Data
return string(data)
}
func (s *Service) AvgUptime() string {
@ -135,11 +132,22 @@ func (u *Service) Delete() error {
col := dbSession.Collection("services")
res := col.Find("id", u.Id)
err := res.Delete()
OnDeletedService(u)
return err
}
func (u *Service) DeleteFailures() {
var fails []*Failure
col := dbSession.Collection("failures")
col.Find("service", u.Id).All(&fails)
for _, fail := range fails {
fail.Delete()
}
}
func (u *Service) Update() {
OnUpdateService(u)
}
func (u *Service) Create() (int64, error) {
@ -150,6 +158,7 @@ func (u *Service) Create() (int64, error) {
if uuid == nil {
return 0, err
}
OnNewService(u)
return uuid.(int64), err
}

View File

@ -20,6 +20,7 @@ type DbConfig struct {
Description string `yaml:"-"`
Username string `yaml:"-"`
Password string `yaml:"-"`
Error error
}
func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
@ -47,24 +48,38 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
description,
username,
password,
nil,
}
err := config.Save()
if err != nil {
throw(err)
config.Error = err
SetupResponseError(w, r, config)
return
}
configs, err = LoadConfig()
if err != nil {
throw(err)
config.Error = err
SetupResponseError(w, r, config)
return
}
err = DbConnection(configs.Connection)
if err != nil {
throw(err)
DeleteConfig()
config.Error = err
SetupResponseError(w, r, config)
return
}
admin := &User{
Username: config.Username,
Password: config.Password,
}
admin.Create()
if sample == "on" {
LoadSampleData()
go LoadSampleData()
}
http.Redirect(w, r, "/", http.StatusSeeOther)
@ -72,6 +87,21 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
mainProcess()
}
func DeleteConfig() {
err := os.Remove("./config.yml")
if err != nil {
throw(err)
}
}
type ErrorResponse struct {
Error string
}
func SetupResponseError(w http.ResponseWriter, r *http.Request, a interface{}) {
ExecuteResponse(w, r, "setup.html", a)
}
func (c *DbConfig) Save() error {
var err error
config, err := os.Create("config.yml")
@ -102,6 +132,8 @@ func (c *DbConfig) Save() error {
"config.yml",
NewSHA1Hash(5),
NewSHA1Hash(10),
"",
"",
VERSION,
[]plugin.Info{},
[]PluginJSON{},

View File

@ -4,6 +4,8 @@ CREATE TABLE core (
config VARCHAR(50),
api_key VARCHAR(50),
api_secret VARCHAR(50),
style text,
footer text,
version VARCHAR(50)
);
CREATE TABLE users (

View File

@ -4,6 +4,8 @@ CREATE TABLE core (
config text,
api_key text,
api_secret text,
style text,
footer text,
version text
);

View File

@ -4,6 +4,8 @@ CREATE TABLE core (
config text,
api_key text,
api_secret text,
style text,
footer text,
version text
);

View File

@ -31,6 +31,12 @@ func SelectUsername(username string) (*User, error) {
return &user, err
}
func (u *User) Delete() error {
col := dbSession.Collection("users")
user := col.Find("id", u.Id)
return user.Delete()
}
func (u *User) Create() (int64, error) {
u.CreatedAt = time.Now()
password := HashPassword(u.Password)
@ -42,6 +48,7 @@ func (u *User) Create() (int64, error) {
if uuid == nil {
return 0, err
}
OnNewUser(u)
return uuid.(int64), err
}

207
web.go
View File

@ -2,6 +2,7 @@ package main
import (
"fmt"
"github.com/fatih/structs"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"html/template"
@ -16,7 +17,7 @@ var (
)
const (
cookieKey = "apizer_auth"
cookieKey = "statup_auth"
)
func Router() *mux.Router {
@ -31,13 +32,15 @@ func Router() *mux.Router {
r.Handle("/logout", http.HandlerFunc(LogoutHandler))
r.Handle("/services", http.HandlerFunc(ServicesHandler)).Methods("GET")
r.Handle("/services", http.HandlerFunc(CreateServiceHandler)).Methods("POST")
r.Handle("/service/{id}", http.HandlerFunc(ServicesViewHandler))
r.Handle("/service/{id}", http.HandlerFunc(ServicesViewHandler)).Methods("GET")
r.Handle("/service/{id}", http.HandlerFunc(ServicesUpdateHandler)).Methods("POST")
r.Handle("/service/{id}/edit", http.HandlerFunc(ServicesViewHandler))
r.Handle("/service/{id}/delete", http.HandlerFunc(ServicesDeleteHandler))
r.Handle("/service/{id}/badge.svg", http.HandlerFunc(ServicesBadgeHandler))
r.Handle("/service/{id}/delete_failures", http.HandlerFunc(ServicesDeleteFailuresHandler)).Methods("GET")
r.Handle("/users", http.HandlerFunc(UsersHandler)).Methods("GET")
r.Handle("/users", http.HandlerFunc(CreateUserHandler)).Methods("POST")
r.Handle("/users/{id}/delete", http.HandlerFunc(UsersDeleteHandler)).Methods("GET")
r.Handle("/settings", http.HandlerFunc(PluginsHandler)).Methods("GET")
r.Handle("/settings", http.HandlerFunc(SaveSettingsHandler)).Methods("POST")
r.Handle("/plugins/download/{name}", http.HandlerFunc(PluginsDownloadHandler))
@ -147,12 +150,11 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
}
func SetupHandler(w http.ResponseWriter, r *http.Request) {
tmpl := Parse("setup.html")
tmpl.Execute(w, nil)
ExecuteResponse(w, r, "setup.html", nil)
}
type index struct {
Project string
Core Core
Services []*Service
}
@ -161,10 +163,8 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/setup", http.StatusSeeOther)
return
}
tmpl := Parse("index.html")
out := index{core.Name, services}
tmpl.Execute(w, out)
out := index{*core, services}
ExecuteResponse(w, r, "index.html", out)
}
type dashboard struct {
@ -178,13 +178,11 @@ type dashboard struct {
func DashboardHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, cookieKey)
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
tmpl := Parse("login.html")
tmpl.Execute(w, nil)
ExecuteResponse(w, r, "login.html", nil)
} else {
tmpl := Parse("dashboard.html")
fails, _ := CountFailures()
out := dashboard{services, core, CountOnline(), len(services), fails}
tmpl.Execute(w, out)
ExecuteResponse(w, r, "dashboard.html", out)
}
}
@ -195,18 +193,17 @@ type serviceHandler struct {
}
func ServicesHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, cookieKey)
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
auth := IsAuthenticated(r)
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
tmpl := Parse("services.html")
tmpl.Execute(w, services)
ExecuteResponse(w, r, "services.html", services)
}
func ServicesDeleteHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, cookieKey)
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
auth := IsAuthenticated(r)
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
@ -218,8 +215,28 @@ func ServicesDeleteHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/services", http.StatusSeeOther)
}
func ServicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
service, _ := SelectService(StringInt(vars["id"]))
service.DeleteFailures()
services, _ = SelectAllServices()
http.Redirect(w, r, "/services", http.StatusSeeOther)
}
func IsAuthenticated(r *http.Request) bool {
session, _ := store.Get(r, cookieKey)
if store == nil {
return false
}
session, err := store.Get(r, cookieKey)
if err != nil {
return false
}
if session.Values["authenticated"] == nil {
return false
}
@ -233,11 +250,24 @@ func SaveSettingsHandler(w http.ResponseWriter, r *http.Request) {
return
}
r.ParseForm()
name := r.PostForm.Get("name")
name := r.PostForm.Get("project")
if name != "" {
core.Name = name
}
description := r.PostForm.Get("description")
core.Name = name
core.Description = description
if description != core.Description {
core.Description = description
}
style := r.PostForm.Get("style")
if style != core.Style {
core.Style = style
}
footer := r.PostForm.Get("footer")
if footer != core.Footer {
core.Footer = footer
}
core.Update()
OnSettingsSaved(core)
http.Redirect(w, r, "/settings", http.StatusSeeOther)
}
@ -247,27 +277,23 @@ func PluginsHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
tmpl := ParsePlugins("plugins.html")
core.FetchPluginRepo()
var pluginFields []PluginSelect
for _, p := range allPlugins {
fields := SelectSettings(p.GetInfo())
fields := structs.Map(p.GetInfo())
pluginFields = append(pluginFields, PluginSelect{p.GetInfo().Name, fields})
}
core.PluginFields = pluginFields
fmt.Println(&core.PluginFields)
tmpl.Execute(w, core)
ExecuteResponse(w, r, "plugins.html", core)
}
type PluginSelect struct {
Plugin string
Params map[string]string
Params map[string]interface{}
}
func PluginSavedHandler(w http.ResponseWriter, r *http.Request) {
@ -283,8 +309,7 @@ func PluginSavedHandler(w http.ResponseWriter, r *http.Request) {
for k, v := range r.PostForm {
data[k] = strings.Join(v, "")
}
UpdateSettings(plug.GetInfo(), data)
plug.OnSave(data)
plug.OnSave(structs.Map(data))
http.Redirect(w, r, "/settings", http.StatusSeeOther)
}
@ -307,8 +332,7 @@ func HelpHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
tmpl := Parse("help.html")
tmpl.Execute(w, nil)
ExecuteResponse(w, r, "help.html", nil)
}
func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
@ -338,63 +362,66 @@ func ServicesBadgeHandler(w http.ResponseWriter, r *http.Request) {
func ServicesViewHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
vars := mux.Vars(r)
service, _ := SelectService(StringInt(vars["id"]))
tmpl := Parse("service.html")
serve := &serviceHandler{service, auth}
tmpl.Execute(w, serve)
}
func Parse(file string) *template.Template {
nav, _ := tmplBox.String("nav.html")
render, err := tmplBox.String(file)
if err != nil {
panic(err)
}
t := template.New("message")
t.Funcs(template.FuncMap{
"js": func(html string) template.JS {
return template.JS(html)
},
"safe": func(html string) template.HTML {
return template.HTML(html)
},
})
t, _ = t.Parse(nav)
t.Parse(render)
return t
}
func ParsePlugins(file string) *template.Template {
nav, _ := tmplBox.String("nav.html")
slack, _ := tmplBox.String("plugins/slack.html")
render, err := tmplBox.String(file)
if err != nil {
panic(err)
}
t := template.New("message")
t.Funcs(template.FuncMap{
"js": func(html string) template.JS {
return template.JS(html)
},
"safe": func(html string) template.HTML {
return template.HTML(html)
},
})
t, _ = t.Parse(nav)
t.Parse(slack)
t.Parse(render)
return t
}
func UsersHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("viewing user")
session, _ := store.Get(r, cookieKey)
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
tmpl := Parse("users.html")
users, _ := SelectAllUsers()
tmpl.Execute(w, users)
vars := mux.Vars(r)
service, _ := SelectService(StringInt(vars["id"]))
ExecuteResponse(w, r, "service.html", service)
}
func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}) {
nav, _ := tmplBox.String("nav.html")
render, err := tmplBox.String(file)
if err != nil {
panic(err)
}
t := template.New("message")
t.Funcs(template.FuncMap{
"js": func(html string) template.JS {
return template.JS(html)
},
"safe": func(html string) template.HTML {
return template.HTML(html)
},
"Auth": func() bool {
return IsAuthenticated(r)
},
"VERSION": func() string {
return VERSION
},
})
t, _ = t.Parse(nav)
t.Parse(render)
t.Execute(w, data)
}
func UsersHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
users, _ := SelectAllUsers()
ExecuteResponse(w, r, "users.html", users)
}
func UsersDeleteHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
user, _ := SelectUser(int64(id))
users, _ := SelectAllUsers()
if len(users) == 1 {
http.Redirect(w, r, "/users", http.StatusSeeOther)
return
}
user.Delete()
http.Redirect(w, r, "/users", http.StatusSeeOther)
}