first commit

pull/10/head
Hunter Long 2018-06-09 18:31:13 -07:00
commit 26b8ab85a5
29 changed files with 1635 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea
rice-box.go

60
.travis.yml Normal file
View File

@ -0,0 +1,60 @@
os:
- linux
language: go
go:
- "1.10.x"
install: true
sudo: required
services:
- docker
env:
- VERSION=0.1
matrix:
allow_failures:
- go: master
fast_finish: true
before_deploy:
- git config --local user.name "hunterlong"
- git config --local user.email "info@socialeck.com"
- git tag "v$VERSION" --force
deploy:
- provider: releases
api_key: $GH_TOKEN
file:
- "build/fusioner-osx-x64"
- "build/fusioner-osx-x32"
- "build/fusioner-linux-x64"
- "build/fusioner-linux-x32"
- "build/fusioner-windows-x64.exe"
- "build/fusioner-windows-x32.exe"
skip_cleanup: true
services:
- postgresql
notifications:
email: false
before_install:
- if [[ "$TRAVIS_BRANCH" == "master" ]]; then travis_wait 30 docker pull karalabe/xgo-latest; fi
before_script:
- go get github.com/stretchr/testify/assert
- go get golang.org/x/tools/cmd/cover
- go get github.com/rendon/testcli
- go get github.com/karalabe/xgo
- go get github.com/GeertJohan/go.rice
- go get github.com/GeertJohan/go.rice/rice
- go get -u golang.org/x/vgo
- vgo get -u
script:
- if [[ "$TRAVIS_BRANCH" == "master" ]]; then /bin/bash -c ./build.sh; fi

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM golang:alpine as builder
RUN apk update && apk add git
COPY . $GOPATH/src/github.com/hunterlong/fusioner/
WORKDIR $GOPATH/src/github.com/hunterlong/fusioner/
RUN go get github.com/GeertJohan/go.rice/rice
RUN go get -d -v
RUN rice embed-go
RUN go install
EXPOSE 8080
ENTRYPOINT fusioner

23
build.sh Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
mkdir build
APP="fusioner"
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 --targets=linux/amd64 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo --targets=linux/386 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo --targets=windows/amd64 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
xgo --targets=windows/386 --dest=build -ldflags="-X main.VERSION=$VERSION" ./
mv build/$APP-darwin-10.6-amd64 build/$APP-osx-x64
mv build/$APP-darwin-10.6-386 build/$APP-osx-x32
mv build/$APP-linux-amd64 build/$APP-linux-x64
mv build/$APP-linux-386 build/$APP-linux-x32
mv build/$APP-windows-4.0-amd64.exe build/$APP-windows-x64.exe
mv build/$APP-windows-4.0-386.exe build/$APP-windows-x32.exe

60
checker.go Normal file
View File

@ -0,0 +1,60 @@
package main
import (
"fmt"
"io/ioutil"
"net/http"
"regexp"
"time"
)
func CheckServices() {
services := SelectAllServices()
for _, v := range services {
obj := v
go obj.CheckQueue()
}
}
func (s *Service) CheckQueue() {
time.Sleep(time.Duration(s.Interval) * time.Second)
s.Check()
fmt.Printf("Service: %v | Online: %v | Latency: %v\n", s.Name, s.Online, s.Latency)
s.CheckQueue()
}
func (s *Service) Check() {
t1 := time.Now()
response, err := http.Get(s.Domain)
t2 := time.Now()
s.Latency = t2.Sub(t1).Seconds()
if err != nil {
s.Failure(response, fmt.Sprintf("HTTP Error %v", err))
return
}
if s.Expected != "" {
contents, _ := ioutil.ReadAll(response.Body)
match, _ := regexp.MatchString(s.Expected, string(contents))
if !match {
s.Failure(response, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected))
return
}
}
if s.ExpectedStatus != response.StatusCode {
s.Failure(response, fmt.Sprintf("HTTP Status Code %v did not match %v", response.StatusCode, s.ExpectedStatus))
return
}
s.Record(response)
}
func (s *Service) Record(response *http.Response) {
defer response.Body.Close()
s.Online = true
db.QueryRow("INSERT INTO hits(service,latency,created_at) VALUES($1,$2,NOW()) returning id;", s.Id, s.Latency).Scan()
}
func (s *Service) Failure(response *http.Response, issue string) {
db.QueryRow("INSERT INTO failures(issue,service,created_at) VALUES($1,$2,NOW()) returning id;", issue, s.Id).Scan()
s.Record(response)
}

26
core.go Normal file
View File

@ -0,0 +1,26 @@
package main
type Core struct {
Name string
Config string
Key string
Secret string
Version string
}
func SelectCore() (*Core, error) {
var core Core
rows, err := db.Query("SELECT * FROM core")
if err != nil {
return nil, err
}
for rows.Next() {
err = rows.Scan(&core.Name, &core.Config, &core.Key, &core.Secret, &core.Version)
if err != nil {
return nil, err
}
}
return &core, err
}

58
database.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
"math/rand"
"time"
)
func DbConnection() {
var err error
dbinfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", configs.Host, configs.Port, configs.User, configs.Password, configs.Database)
db, err = sql.Open("postgres", dbinfo)
if err != nil {
panic(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")
db.QueryRow(upgrade).Scan()
}
func DropDatabase() {
fmt.Println("Dropping Tables...")
down, _ := sqlBox.String("down.sql")
db.QueryRow(down).Scan()
}
func CreateDatabase() {
fmt.Println("Creating Tables...")
VERSION = "1.1.1"
up, _ := sqlBox.String("up.sql")
db.QueryRow(up).Scan()
//secret := NewSHA1Hash()
//db.QueryRow("INSERT INTO core (secret, version) VALUES ($1, $2);", secret, VERSION).Scan()
fmt.Println("Database Created")
//SampleData()
}
func SampleData() {
i := 0
for i < 300 {
ran := rand.Float32()
latency := fmt.Sprintf("%0.2f", ran)
date := time.Now().AddDate(0, 0, i)
db.QueryRow("INSERT INTO hits (service, latency, created_at) VALUES (1, $1, $2);", latency, date).Scan()
i++
}
}

33
failures.go Normal file
View File

@ -0,0 +1,33 @@
package main
import "time"
type Failure struct {
Id int
Issue string
Service int
CreatedAt time.Time
}
func SelectAllFailures(id int64) []float64 {
var tks []float64
rows, err := db.Query("SELECT * FROM failures WHERE service=$1 ORDER BY id ASC", id)
if err != nil {
panic(err)
}
for rows.Next() {
var tk Hit
err = rows.Scan(&tk.Id, &tk.Metric, &tk.Value, &tk.CreatedAt)
if err != nil {
panic(err)
}
tks = append(tks, tk.Value)
}
return tks
}
func (s *Service) TotalFailures() int {
var amount int
db.QueryRow("SELECT COUNT(id) FROM failures WHERE service=$1;", s.Id).Scan(&amount)
return amount
}

11
go.mod Normal file
View File

@ -0,0 +1,11 @@
module github.com/hunterlong/apizer
require (
github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da
github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb
github.com/go-yaml/yaml v0.0.0-20180328195020-5420a8b6744d
github.com/gorilla/sessions v1.1.1
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
github.com/lib/pq v0.0.0-20180523175426-90697d60dd84
golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4
)

39
hits.go Normal file
View File

@ -0,0 +1,39 @@
package main
import "time"
type Hit struct {
Id int
Metric int
Value float64
CreatedAt time.Time
}
func SelectAllHits(id int64) []Hit {
var tks []Hit
rows, err := db.Query("SELECT * FROM hits WHERE service=$1 ORDER BY id ASC", id)
if err != nil {
panic(err)
}
for rows.Next() {
var tk Hit
err = rows.Scan(&tk.Id, &tk.Metric, &tk.Value, &tk.CreatedAt)
if err != nil {
panic(err)
}
tks = append(tks, tk)
}
return tks
}
func (s *Service) TotalHits() int {
var amount int
db.QueryRow("SELECT COUNT(id) FROM hits WHERE service=$1;", s.Id).Scan(&amount)
return amount
}
func (s *Service) Sum() float64 {
var amount float64
db.QueryRow("SELECT SUM(latency) FROM hits WHERE service=$1;", s.Id).Scan(&amount)
return amount
}

27
html/css/base.css Normal file
View File

@ -0,0 +1,27 @@
HTML,BODY {
background-color: #efefef;
}
.container {
max-width: 790px;
margin-top: 40px;
background-color: white;
padding: 50px;
border-radius: 7px;
}
.navbar {
margin-left: -50px;
width: 720px;
margin-bottom: 30px;
}
.lg_number {
font-size: 26pt;
font-weight: bold;
display: block;
}
.stats_area {
text-align: center;
}

7
html/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

67
html/index.html Normal file
View File

@ -0,0 +1,67 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/base.css">
<title>Hello, world!</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12">
<form action="/login" method="POST">
<div class="form-group row">
<label for="inputEmail3" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" name="username" class="form-control" id="inputEmail3" placeholder="Username">
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" name="password" class="form-control" id="inputPassword3" placeholder="Password">
</div>
</div>
<div class="form-group row">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="gridCheck1">
<label class="form-check-label" for="gridCheck1">
Remember this computer
</label>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Sign in</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="/js/jquery-3.3.1.slim.min.js"></script>
</body>
</html>

10
html/js/Chart.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
html/js/jquery-3.3.1.slim.min.js vendored Normal file

File diff suppressed because one or more lines are too long

67
html/tmpl/dashboard.html Normal file
View File

@ -0,0 +1,67 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/base.css">
<title>Fusioner | Dashboard</title>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">Fusioner</a>
<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 active">
<a class="nav-link" href="/dashboard">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/tokens">Tokens</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/users">Users</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/permissions">Permissions</a>
</li>
</ul>
<span class="navbar-text">
<a class="nav-link" href="/logout">Logout</a>
</span>
</div>
</nav>
<div class="row stats_area">
<div class="col-4">
<span class="lg_number">69</span>
24 Hour Hits
</div>
<div class="col-4">
<span class="lg_number">3921</span>
24 Hour Hits
</div>
<div class="col-4">
<span class="lg_number">453</span>
Total Tokens
</div>
</div>
<script src="/js/jquery-3.3.1.slim.min.js"></script>
</body>
</html>

109
html/tmpl/index.html Normal file
View File

@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/base.css">
<script src="/js/Chart.bundle.min.js"></script>
<title>Fusioner | Dashboard</title>
</head>
<body>
<div class="container">
<div class="row">
{{ range .Services }}
<div class="col-12 mb-4">
<div class="card">
<div class="card-body">
<h3>{{ .Name }} <span class="badge badge-secondary float-right">{{if .Online}} ONLINE {{ else }} OFFLINE {{end}}</span></h3>
<div class="row stats_area mt-3 mb-3">
<div class="col-4">
<span class="lg_number">{{.Online24Hours}}%</span>
Online last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{.AvgResponse}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{.TotalUptime}}%</span>
Total Uptime
</div>
</div>
<canvas id="service_{{ .Id }}" width="400" height="120"></canvas>
</div>
</div>
</div>
{{ end }}
</div>
</div>
<script>
{{ range .Services }}
var ctx = document.getElementById("service_{{.Id}}").getContext('2d');
var chartdata = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: 'Response Time (Milliseconds)',
data: {{js .Data}},
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255,99,132,1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
legend: {
display: false
},
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}],
xAxes: [{
type: 'time',
distribution: 'series'
}]
},
elements: {
point: {
radius: 0
}
}
}
});
{{ end }}
</script>
<script src="/js/jquery-3.3.1.slim.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,95 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/base.css">
<title>Fusioner | Permissions</title>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">Fusioner</a>
<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 class="nav-link" href="/dashboard">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/services">Services</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/users">Users</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/permissions">Permissions</a>
</li>
</ul>
<span class="navbar-text">
<a class="nav-link" href="/logout">Logout</a>
</span>
</div>
</nav>
<div class="row">
<div class="col-12">
<h3>Users</h3>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Username</th>
</tr>
</thead>
<tbody>
{{range .}}
<tr>
<th scope="row">{{.Id}}</th>
<td>{{.Username}}</td>
</tr>
{{end}}
</tbody>
</table>
<form action="/user/create" method="POST">
<div class="form-group row">
<label for="inputEmail3" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" name="username" class="form-control" id="inputEmail3" placeholder="Username">
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" name="password" class="form-control" id="inputPassword3" placeholder="Password">
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Create User</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="/js/jquery-3.3.1.slim.min.js"></script>
</body>
</html>

83
html/tmpl/services.html Normal file
View File

@ -0,0 +1,83 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/base.css">
<title>Fusioner | Tokens</title>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">Fusioner</a>
<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 class="nav-link" href="/dashboard">Dashboard</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/tokens">Tokens</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/users">Users</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/permissions">Permissions</a>
</li>
</ul>
<span class="navbar-text">
<a class="nav-link" href="/logout">Logout</a>
</span>
</div>
</nav>
<div class="row">
<div class="col-12">
<h3>Tokens</h3>
<a href="/token/create" class="btn btn-primary">Create New Token</a>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Key</th>
<th scope="col">Secret</th>
</tr>
</thead>
<tbody>
{{range .}}
<tr>
<th scope="row">{{.Id}}</th>
<td>{{.Key}}</td>
<td>{{.Secret}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
<script src="/js/jquery-3.3.1.slim.min.js"></script>
</body>
</html>

96
html/tmpl/setup.html Normal file
View File

@ -0,0 +1,96 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/base.css">
<script src="/js/Chart.bundle.min.js"></script>
<title>Fusioner | Setup</title>
</head>
<body>
<div class="container">
<form method="POST" action="/setup/save">
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="inputState">Database Connection</label>
<select id="inputState" name="db_connection" class="form-control">
<option selected value="postgres">Postgres</option>
<option value="mysql">MySQL</option>
</select>
</div>
<div class="form-group">
<label for="formGroupExampleInput">Host</label>
<input type="text" name="db_host" class="form-control" id="formGroupExampleInput" value="localhost" placeholder="localhost">
</div>
<div class="form-group">
<label for="formGroupExampleInput">Database Port</label>
<input type="text" name="db_port" class="form-control" id="formGroupExampleInput" value="5555" placeholder="localhost">
</div>
<div class="form-group">
<label for="formGroupExampleInput2">Username</label>
<input type="text" name="db_user" class="form-control" id="formGroupExampleInput2" value="root" placeholder="root">
</div>
<div class="form-group">
<label for="formGroupExampleInput2">Password</label>
<input type="password" name="db_password" class="form-control" id="formGroupExampleInput2" value="223352hunter" placeholder="password123">
</div>
<div class="form-group">
<label for="formGroupExampleInput2">Database</label>
<input type="text" name="db_database" class="form-control" id="formGroupExampleInput2" value="uptime" placeholder="uptimeapi">
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="formGroupExampleInput">Project Name</label>
<input type="text" name="project" class="form-control" 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">
</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">
</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">
</div>
<div class="form-group">
<label class="form-check-label" for="gridCheck1">
Load Sample Data
</label>
<input name="sample_data" class="form-control" type="checkbox" id="gridCheck1" checked>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Save Settings</button>
</div>
</form>
</div>
<script src="/js/jquery-3.3.1.slim.min.js"></script>
</body>
</html>

96
html/tmpl/users.html Normal file
View File

@ -0,0 +1,96 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/base.css">
<title>Fusioner | Users</title>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">Fusioner</a>
<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 class="nav-link" href="/dashboard">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/tokens">Tokens</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/users">Users</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/permissions">Permissions</a>
</li>
</ul>
<span class="navbar-text">
<a class="nav-link" href="/logout">Logout</a>
</span>
</div>
</nav>
<div class="row">
<div class="col-12">
<h3>Users</h3>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Username</th>
</tr>
</thead>
<tbody>
{{range .}}
<tr>
<th scope="row">{{.Id}}</th>
<td>{{.Username}}</td>
</tr>
{{end}}
</tbody>
</table>
<form action="/user/create" method="POST">
<div class="form-group row">
<label for="inputEmail3" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" name="username" class="form-control" id="inputEmail3" placeholder="Username">
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" name="password" class="form-control" id="inputPassword3" placeholder="Password">
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Create User</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="/js/jquery-3.3.1.slim.min.js"></script>
</body>
</html>

86
main.go Normal file
View File

@ -0,0 +1,86 @@
package main
import (
"database/sql"
"github.com/GeertJohan/go.rice"
"github.com/go-yaml/yaml"
"github.com/gorilla/sessions"
_ "github.com/lib/pq"
"golang.org/x/crypto/bcrypt"
"io/ioutil"
"fmt"
)
var (
db *sql.DB
configs *Config
core *Core
store *sessions.CookieStore
VERSION string
sqlBox *rice.Box
cssBox *rice.Box
jsBox *rice.Box
tmplBox *rice.Box
setupMode bool
)
type Config struct {
Connection string `yaml:"connection"`
Host string `yaml:"host"`
Database string `yaml:"database"`
User string `yaml:"user"`
Password string `yaml:"password"`
Port string `yaml:"port"`
Secret string `yaml:"secret"`
}
func main() {
VERSION = "1.1.1"
RenderBoxes()
configs = LoadConfig()
if configs == nil {
fmt.Println("config.yml file not found - starting in setup mode")
setupMode = true
RunHTTPServer()
}
mainProcess()
}
func mainProcess() {
var err error
DbConnection()
core, err = SelectCore()
if err != nil {
panic(err)
}
go CheckServices()
if !setupMode {
RunHTTPServer()
}
}
func RenderBoxes() {
sqlBox = rice.MustFindBox("sql")
cssBox = rice.MustFindBox("html/css")
jsBox = rice.MustFindBox("html/js")
tmplBox = rice.MustFindBox("html/tmpl")
}
func LoadConfig() *Config {
var config Config
file, err := ioutil.ReadFile("config.yml")
if err != nil {
return nil
}
yaml.Unmarshal(file, &config)
store = sessions.NewCookieStore([]byte(config.Secret))
return &config
}
func HashPassword(password string) string {
bytes, _ := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes)
}

144
services.go Normal file
View File

@ -0,0 +1,144 @@
package main
import (
"crypto/sha1"
"encoding/json"
"fmt"
"math/rand"
"time"
)
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 float64
AvgResponse string
TotalUptime float64
}
func SelectService(id string) Service {
var tk Service
rows, err := db.Query("SELECT * FROM services WHERE id=$1", id)
if err != nil {
panic(err)
}
for rows.Next() {
err = rows.Scan(&tk.Id, &tk.Name, &tk.Domain, &tk.Method, &tk.Port, &tk.Expected, &tk.ExpectedStatus, &tk.Interval, &tk.CreatedAt)
if err != nil {
panic(err)
}
}
return tk
}
func SelectAllServices() []Service {
var tks []Service
rows, err := db.Query("SELECT * FROM services ORDER BY id ASC")
if err != nil {
panic(err)
}
for rows.Next() {
var tk Service
err = rows.Scan(&tk.Id, &tk.Name, &tk.Domain, &tk.Method, &tk.Port, &tk.Expected, &tk.ExpectedStatus, &tk.Interval, &tk.CreatedAt)
if err != nil {
panic(err)
}
tk.FormatData()
tks = append(tks, tk)
}
return tks
}
func (s *Service) FormatData() *Service {
s.GraphData()
s.AvgUptime()
s.AvgTime()
return s
}
func (s *Service) AvgTime() float64 {
total := s.TotalHits()
sum := s.Sum()
avg := sum / float64(total) * 100
s.AvgResponse = fmt.Sprintf("%0.0f", avg*10)
return avg
}
type GraphJson struct {
X string `json:"x"`
Y float64 `json:"y"`
}
func (s *Service) GraphData() string {
hits := SelectAllHits(s.Id)
var d []*GraphJson
for _, h := range hits {
val := h.CreatedAt
o := &GraphJson{
X: val.String(),
Y: h.Value * 1000,
}
d = append(d, o)
}
data, _ := json.Marshal(d)
s.Data = string(data)
return s.Data
}
func (s *Service) AvgUptime() float64 {
failed := s.TotalFailures()
total := s.TotalHits()
if failed == 0 {
s.TotalUptime = 100.00
return s.TotalUptime
}
percent := float64(failed) / float64(total) * 100
fmt.Println(failed, total, percent)
s.TotalUptime = percent
return percent
}
func (u *Service) Create() int {
var lastInsertId int
db.QueryRow("INSERT INTO services(name, domain, expected, expected_status, created_at) VALUES($1,$2,$3,$4,NOW()) returning id;", u.Name, u.Domain, u.Expected, u.ExpectedStatus).Scan(&lastInsertId)
return lastInsertId
}
// NewSHA1Hash generates a new SHA1 hash based on
// a random number of characters.
func NewSHA1Hash(n ...int) string {
noRandomCharacters := 32
if len(n) > 0 {
noRandomCharacters = n[0]
}
randString := RandomString(noRandomCharacters)
hash := sha1.New()
hash.Write([]byte(randString))
bs := hash.Sum(nil)
return fmt.Sprintf("%x", bs)
}
var characterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// RandomString generates a random string of n length
func RandomString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = characterRunes[rand.Intn(len(characterRunes))]
}
return string(b)
}

80
setup.go Normal file
View File

@ -0,0 +1,80 @@
package main
import (
"net/http"
"strconv"
"os"
"github.com/go-yaml/yaml"
"time"
)
type DbConfig struct {
DbConn string `yaml:"connection"`
DbHost string `yaml:"host"`
DbUser string `yaml:"user"`
DbPass string `yaml:"password"`
DbData string `yaml:"database"`
DbPort int `yaml:"port"`
Project string `yaml:"-"`
Username string `yaml:"-"`
Password string `yaml:"-"`
}
func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
dbHost := r.PostForm.Get("db_host")
dbUser := r.PostForm.Get("db_user")
dbPass := r.PostForm.Get("db_password")
dbDatabase := r.PostForm.Get("db_database")
dbConn := r.PostForm.Get("db_connection")
dbPort, _ := strconv.Atoi(r.PostForm.Get("db_port"))
project := r.PostForm.Get("project")
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
config := &DbConfig{
dbConn,
dbHost,
dbUser,
dbPass,
dbDatabase,
dbPort,
project,
username,
password,
}
err := config.Save()
if err != nil {
panic(err)
}
http.Redirect(w, r, "/", http.StatusSeeOther)
time.Sleep(2 * time.Second)
mainProcess()
}
func (c *DbConfig) Save() error {
var err error
config, err := os.Create("config.yml")
if err != nil {
return err
}
data, err := yaml.Marshal(c)
if err != nil {
return err
}
config.WriteString(string(data))
config.Close()
configs = LoadConfig()
DbConnection()
DropDatabase()
CreateDatabase()
db.QueryRow("INSERT INTO core (name, config, api_key, api_secret, version) VALUES($1,$2,$3,$4,$5);", c.Project, "config.yml", NewSHA1Hash(5), NewSHA1Hash(10), VERSION).Scan()
return err
}

5
sql/down.sql Normal file
View File

@ -0,0 +1,5 @@
DROP table core;
DROP table hits;
DROP table failures;
DROP table services;
DROP table users;

60
sql/up.sql Normal file
View File

@ -0,0 +1,60 @@
CREATE TABLE core (
name text,
config text,
api_key text,
api_secret text,
version text
);
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username text,
password text,
key text,
secret text,
created_at TIMESTAMP WITHOUT TIME zone
);
CREATE TABLE services (
id SERIAL PRIMARY KEY,
name text,
domain text,
method text,
port integer,
expected text,
expected_status integer,
interval integer,
created_at TIMESTAMP WITHOUT TIME zone
);
CREATE TABLE hits (
id SERIAL PRIMARY KEY,
service INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE ON UPDATE CASCADE,
latency float,
created_at TIMESTAMP WITHOUT TIME zone
);
CREATE TABLE failures (
id SERIAL PRIMARY KEY,
issue text,
service INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMP WITHOUT TIME zone
);
CREATE INDEX idx_hits ON hits(service);
CREATE INDEX idx_failures ON failures(service);
INSERT INTO users (id, username, password, created_at) VALUES (1, 'admin', '$2a$14$sBO5VDKiGPNUa3IUSMRX.OJNIbw/VM5dXOzTjlsjvG6qA987Lfzga', NOW());
INSERT INTO services (id, name, domain, method, port, expected, expected_status, interval, created_at) VALUES (1, 'Google', 'https://www.google.com', 'https', 0, '', 200, 5, NOW());
INSERT INTO services (id, name, domain, method, port, expected, expected_status, interval, created_at) VALUES (2, 'Github', 'https://github.com', 'https', 0, '', 200, 10, NOW());
INSERT INTO services (id, name, domain, method, port, expected, expected_status, interval, created_at) VALUES (3, 'Santa Monica', 'https://www.santamonica.com', 'https', 0, '', 200, 30, NOW());
INSERT INTO services (id, name, domain, method, port, expected, expected_status, interval, created_at) VALUES (4, 'Example JSON', 'https://jsonplaceholder.typicode.com/posts/42', 'https', 0, 'userId', 200, 5, NOW());
INSERT INTO services (id, name, domain, method, port, expected, expected_status, interval, created_at) VALUES (5, 'Token Balance API', 'https://api.tokenbalance.com/token/0x86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0/0x5c98dc37c0b3ef75476eb6b8ef0d564f7c6af6ae', 'https', 0, 'broken', 200, 8, NOW());
INSERT INTO services (id, name, domain, method, port, expected, expected_status, interval, created_at) VALUES (6, 'Example JSON 2', 'https://jsonplaceholder.typicode.com/posts/42', 'https', 0, 'commodi ullam sint et excepturi error explicabo praesentium voluptas', 200, 13, NOW());

0
sql/upgrade.sql Normal file
View File

66
users.go Normal file
View File

@ -0,0 +1,66 @@
package main
import (
"golang.org/x/crypto/bcrypt"
)
type User struct {
Id int64
Username string
Password string
Email string
}
func SelectUser(username string) User {
var user User
rows, err := db.Query("SELECT id, username, password FROM users WHERE username=$1", username)
if err != nil {
panic(err)
}
for rows.Next() {
err = rows.Scan(&user.Id, &user.Username, &user.Password)
if err != nil {
panic(err)
}
}
return user
}
func (u *User) Create() int {
password := HashPassword(u.Password)
var lastInsertId int
db.QueryRow("INSERT INTO users(username,password,created_at) VALUES($1,$2,NOW()) returning id;", u.Username, password).Scan(&lastInsertId)
return lastInsertId
}
func SelectAllUsers() []User {
var users []User
rows, err := db.Query("SELECT id, username, password FROM users ORDER BY id ASC")
if err != nil {
panic(err)
}
for rows.Next() {
var user User
err = rows.Scan(&user.Id, &user.Username, &user.Password)
if err != nil {
panic(err)
}
users = append(users, user)
}
return users
}
func AuthUser(username, password string) (User, bool) {
var user User
var auth bool
user = SelectUser(username)
if CheckHash(password, user.Password) {
auth = true
}
return user, auth
}
func CheckHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

209
web.go Normal file
View File

@ -0,0 +1,209 @@
package main
import (
"fmt"
"html/template"
"net/http"
)
type dashboard struct {
Services []Service
Users []User
Core *Core
}
func RunHTTPServer() {
fmt.Println("Fusioner HTTP Server running on http://localhost:9595")
css := http.StripPrefix("/css/", http.FileServer(cssBox.HTTPBox()))
js := http.StripPrefix("/js/", http.FileServer(jsBox.HTTPBox()))
http.Handle("/", http.HandlerFunc(IndexHandler))
http.Handle("/css/", css)
http.Handle("/js/", js)
http.Handle("/setup", http.HandlerFunc(SetupHandler))
http.Handle("/setup/save", http.HandlerFunc(ProcessSetupHandler))
http.Handle("/dashboard", http.HandlerFunc(DashboardHandler))
http.Handle("/login", http.HandlerFunc(LoginHandler))
http.Handle("/logout", http.HandlerFunc(LogoutHandler))
//http.Handle("/auth", http.HandlerFunc(AuthenticateHandler))
http.Handle("/user/create", http.HandlerFunc(CreateUserHandler))
http.Handle("/token/create", http.HandlerFunc(CreateServiceHandler))
http.Handle("/tokens", http.HandlerFunc(ServicesHandler))
http.Handle("/users", http.HandlerFunc(UsersHandler))
http.ListenAndServe(":9595", nil)
}
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "apizer_auth")
session.Values["authenticated"] = false
session.Save(r, w)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func LoginHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "apizer_auth")
r.ParseForm()
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
_, auth := AuthUser(username, password)
if auth {
session.Values["authenticated"] = true
session.Save(r, w)
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
} else {
w.WriteHeader(502)
w.Header().Set("Content-Type", "plain/text")
fmt.Fprintln(w, "bad")
}
}
//func AuthenticateHandler(w http.ResponseWriter, r *http.Request) {
// r.ParseForm()
// key := r.PostForm.Get("key")
// secret := r.PostForm.Get("secret")
// token := SelectToken(key, secret)
// if token.Id != 0 {
// go token.Hit(r)
// w.WriteHeader(200)
// w.Header().Set("Content-Type", "plain/text")
// fmt.Fprintln(w, token.Id)
// } else {
// w.WriteHeader(502)
// w.Header().Set("Content-Type", "plain/text")
// fmt.Fprintln(w, "bad")
// }
//}
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
user := &User{
Username: username,
Password: password,
}
user.Create()
http.Redirect(w, r, "/users", http.StatusSeeOther)
}
func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
token := &Service{}
token.Create()
http.Redirect(w, r, "/services", http.StatusSeeOther)
}
func SetupHandler(w http.ResponseWriter, r *http.Request) {
setupFile, err := tmplBox.String("setup.html")
if err != nil {
panic(err)
}
setupTmpl, err := template.New("message").Parse(setupFile)
if err != nil {
panic(err)
}
setupTmpl.Execute(w, nil)
}
type index struct {
Services []Service
}
func IndexHandler(w http.ResponseWriter, r *http.Request) {
//session, _ := store.Get(r, "apizer_auth")
//if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
// http.Redirect(w, r, "/", http.StatusSeeOther)
// return
//}
if setupMode {
http.Redirect(w, r, "/setup", http.StatusSeeOther)
return
}
dashboardFile, err := tmplBox.String("index.html")
if err != nil {
panic(err)
}
dashboardTmpl, err := template.New("message").Funcs(template.FuncMap{
"js": func(html string) template.JS {
return template.JS(html)
},
}).Parse(dashboardFile)
out := index{SelectAllServices()}
dashboardTmpl.Execute(w, out)
}
func DashboardHandler(w http.ResponseWriter, r *http.Request) {
//session, _ := store.Get(r, "apizer_auth")
//if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
// http.Redirect(w, r, "/", http.StatusSeeOther)
// return
//}
dashboardFile, err := tmplBox.String("dashboard.html")
if err != nil {
panic(err)
}
dashboardTmpl, err := template.New("message").Parse(dashboardFile)
if err != nil {
panic(err)
}
out := dashboard{SelectAllServices(), SelectAllUsers(), core}
dashboardTmpl.Execute(w, out)
}
func ServicesHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "apizer_auth")
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
tokensFile, err := tmplBox.String("services.html")
if err != nil {
panic(err)
}
tokensTmpl, err := template.New("message").Parse(tokensFile)
if err != nil {
panic(err)
}
tokensTmpl.Execute(w, SelectAllServices())
}
func UsersHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "apizer_auth")
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
usersFile, err := tmplBox.String("users.html")
if err != nil {
panic(err)
}
usersTmpl, err := template.New("message").Parse(usersFile)
if err != nil {
panic(err)
}
usersTmpl.Execute(w, SelectAllUsers())
}
func PermissionsHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "apizer_auth")
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
permsFile, err := tmplBox.String("permissions.html")
if err != nil {
panic(err)
}
permsTmpl, err := template.New("message").Parse(permsFile)
if err != nil {
panic(err)
}
permsTmpl.Execute(w, SelectAllUsers())
}