mirror of https://github.com/statping/statping
first commit
commit
26b8ab85a5
|
@ -0,0 +1,2 @@
|
||||||
|
.idea
|
||||||
|
rice-box.go
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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++
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -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>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
DROP table core;
|
||||||
|
DROP table hits;
|
||||||
|
DROP table failures;
|
||||||
|
DROP table services;
|
||||||
|
DROP table users;
|
|
@ -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,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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
Loading…
Reference in New Issue