password fix - Messages for service - fixed null bool/string - better date lib

pull/94/head
Hunter Long 2018-11-06 21:06:44 -08:00
parent 52756ee359
commit 6c28635763
40 changed files with 774 additions and 1582 deletions

View File

@ -165,7 +165,7 @@ func (s *Service) checkHttp(record bool) *Service {
var response *http.Response
if s.Method == "POST" {
response, err = client.Post(s.Domain, "application/json", bytes.NewBuffer([]byte(s.PostData)))
response, err = client.Post(s.Domain, "application/json", bytes.NewBuffer([]byte(s.PostData.String)))
} else {
response, err = client.Get(s.Domain)
}
@ -190,11 +190,11 @@ func (s *Service) checkHttp(record bool) *Service {
s.LastResponse = string(contents)
s.LastStatusCode = response.StatusCode
if s.Expected != "" {
if s.Expected.String != "" {
if err != nil {
utils.Log(2, err)
}
match, err := regexp.MatchString(s.Expected, string(contents))
match, err := regexp.MatchString(s.Expected.String, string(contents))
if err != nil {
utils.Log(2, err)
}

View File

@ -16,6 +16,7 @@
package core
import (
"database/sql"
"errors"
"fmt"
"github.com/go-yaml/yaml"
@ -71,9 +72,8 @@ func LoadUsingEnv() (*DbConfig, error) {
CoreApp.Name = os.Getenv("NAME")
CoreApp.Domain = os.Getenv("DOMAIN")
CoreApp.DbConnection = Configs.DbConn
if os.Getenv("USE_CDN") == "true" {
CoreApp.UseCdn = true
}
CoreApp.UseCdn = sql.NullBool{os.Getenv("USE_CDN") == "true", true}
err := Configs.Connect(true, utils.Directory)
if err != nil {
utils.Log(4, err)
@ -94,7 +94,7 @@ func LoadUsingEnv() (*DbConfig, error) {
Username: "admin",
Password: "admin",
Email: "info@admin.com",
Admin: true,
Admin: sql.NullBool{true, true},
})
_, err := admin.Create()

View File

@ -16,6 +16,7 @@
package core
import (
"database/sql"
"errors"
"github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/source"
@ -147,9 +148,7 @@ func SelectCore() (*Core, error) {
}
CoreApp.DbConnection = Configs.DbConn
CoreApp.Version = VERSION
if os.Getenv("USE_CDN") == "true" {
CoreApp.UseCdn = true
}
CoreApp.UseCdn = sql.NullBool{os.Getenv("USE_CDN") == "true", true}
return CoreApp, db.Error
}

View File

@ -68,6 +68,11 @@ func checkinDB() *gorm.DB {
return DbSession.Model(&types.Checkin{})
}
// messagesDb returns the Checkin records for a service
func messagesDb() *gorm.DB {
return DbSession.Model(&types.Message{})
}
// checkinHitsDB returns the 'hits' from the Checkin record
func checkinHitsDB() *gorm.DB {
return DbSession.Model(&types.CheckinHit{})
@ -329,6 +334,7 @@ func (db *DbConfig) DropDatabase() error {
err = DbSession.DropTableIfExists("hits")
err = DbSession.DropTableIfExists("services")
err = DbSession.DropTableIfExists("users")
err = DbSession.DropTableIfExists("messages")
return err.Error
}
@ -343,6 +349,7 @@ func (db *DbConfig) CreateDatabase() error {
err = DbSession.CreateTable(&types.Hit{})
err = DbSession.CreateTable(&types.Service{})
err = DbSession.CreateTable(&types.User{})
err = DbSession.CreateTable(&types.Message{})
utils.Log(1, "Statup Database Created")
return err.Error
}
@ -361,7 +368,7 @@ func (db *DbConfig) MigrateDatabase() error {
if tx.Error != nil {
return tx.Error
}
tx = tx.AutoMigrate(&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Checkin{}, &types.CheckinHit{}, &notifier.Notification{}).Table("core").AutoMigrate(&types.Core{})
tx = tx.AutoMigrate(&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Message{}, &types.Checkin{}, &types.CheckinHit{}, &notifier.Notification{}).Table("core").AutoMigrate(&types.Core{})
if tx.Error != nil {
tx.Rollback()
utils.Log(3, fmt.Sprintf("Statup Database could not be migrated: %v", tx.Error))

View File

@ -17,6 +17,7 @@ package core
import (
"bytes"
"database/sql"
"fmt"
"github.com/hunterlong/statup/source"
"github.com/hunterlong/statup/utils"
@ -32,7 +33,7 @@ func ExportIndexHTML() string {
source.Assets()
injectDatabase()
CoreApp.SelectAllServices(false)
CoreApp.UseCdn = true
CoreApp.UseCdn = sql.NullBool{true, true}
for _, srv := range CoreApp.Services {
service := srv.(*Service)
service.Check(true)
@ -60,7 +61,7 @@ func ExportIndexHTML() string {
return CoreApp
},
"USE_CDN": func() bool {
return CoreApp.UseCdn
return CoreApp.UseCdn.Bool
},
"underscore": func(html string) string {
return utils.UnderScoreString(html)

80
core/messages.go Normal file
View File

@ -0,0 +1,80 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"time"
)
type Message struct {
*types.Message
}
// SelectServiceMessages returns all messages for a service
func SelectServiceMessages(id int64) []*Message {
var message []*Message
messagesDb().Where("service = ?", id).Limit(10).Find(&message)
return message
}
// ReturnMessage will convert *types.Message to *core.Message
func ReturnMessage(m *types.Message) *Message {
return &Message{m}
}
// SelectMessages returns all messages
func SelectMessages() ([]*Message, error) {
var messages []*Message
db := messagesDb().Find(&messages).Order("id desc")
return messages, db.Error
}
// SelectMessage returns a Message based on the ID passed
func SelectMessage(id int64) (*Message, error) {
var message Message
db := messagesDb().Where("id = ?", id).Find(&message)
return &message, db.Error
}
// Create will create a Message and insert it into the database
func (m *Message) Create() (int64, error) {
m.CreatedAt = time.Now().UTC()
db := messagesDb().Create(m)
if db.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to create message %v #%v: %v", m.Title, m.Id, db.Error))
return 0, db.Error
}
return m.Id, nil
}
// Delete will delete a Message from database
func (m *Message) Delete() error {
db := messagesDb().Delete(m)
return db.Error
}
// Update will update a Message in the database
func (m *Message) Update() (*Message, error) {
db := messagesDb().Update(m)
if db.Error != nil {
utils.Log(3, fmt.Sprintf("Failed to update message %v #%v: %v", m.Title, m.Id, db.Error))
return nil, db.Error
}
return m, nil
}

View File

@ -16,6 +16,7 @@
package core
import (
"database/sql"
"fmt"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
@ -60,11 +61,11 @@ func InsertSampleData() error {
Name: "JSON API Tester",
Domain: "https://jsonplaceholder.typicode.com/posts",
ExpectedStatus: 201,
Expected: `(title)": "((\\"|[statup])*)"`,
Expected: utils.NullString(`(title)": "((\\"|[statup])*)"`),
Interval: 30,
Type: "http",
Method: "POST",
PostData: `{ "title": "statup", "body": "bar", "userId": 19999 }`,
PostData: utils.NullString(`{ "title": "statup", "body": "bar", "userId": 19999 }`),
Timeout: 30,
Order: 4,
})
@ -155,7 +156,7 @@ func insertSampleCore() error {
Domain: "http://localhost:8080",
Version: "test",
CreatedAt: time.Now(),
UseCdn: false,
UseCdn: sql.NullBool{false, true},
}
query := coreDB().Create(core)
return query.Error
@ -167,14 +168,14 @@ func insertSampleUsers() {
Username: "testadmin",
Password: "password123",
Email: "info@betatude.com",
Admin: true,
Admin: sql.NullBool{true, true},
})
u3 := ReturnUser(&types.User{
Username: "testadmin2",
Password: "password123",
Email: "info@adminhere.com",
Admin: true,
Admin: sql.NullBool{true, true},
})
u2.Create()

View File

@ -394,6 +394,12 @@ func (s *Service) Create(check bool) (int64, error) {
return s.Id, nil
}
// Messages returns all Messages for a Service
func (s *Service) Messages() []*Message {
messages := SelectServiceMessages(s.Id)
return messages
}
// ServicesCount returns the amount of services inside the []*core.Services slice
func (c *Core) ServicesCount() int {
return len(c.Services)

View File

@ -35,7 +35,7 @@ func ReturnUser(u *types.User) *user {
// SelectUser returns the user based on the user's ID.
func SelectUser(id int64) (*user, error) {
var user user
err := usersDB().First(&user, id)
err := usersDB().Where("id = ?", id).First(&user)
return &user, err.Error
}
@ -54,7 +54,6 @@ func (u *user) Delete() error {
// Update will update the user's record in database
func (u *user) Update() error {
u.Password = utils.HashPassword(u.Password)
u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10)
return usersDB().Update(u).Error

View File

@ -108,8 +108,11 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap
"len": func(g []types.ServiceInterface) int {
return len(g)
},
"IsNil": func(g interface{}) bool {
return g == nil
},
"USE_CDN": func() bool {
return core.CoreApp.UseCdn
return core.CoreApp.UseCdn.Bool
},
"Type": func(g interface{}) []string {
fooType := reflect.TypeOf(g)
@ -163,6 +166,9 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap
"NewCheckin": func() *types.Checkin {
return new(types.Checkin)
},
"NewMessage": func() *types.Message {
return new(types.Message)
},
}
}
@ -176,7 +182,7 @@ func executeResponse(w http.ResponseWriter, r *http.Request, file string, data i
return
}
templates := []string{"base.html", "head.html", "nav.html", "footer.html", "scripts.html", "form_service.html", "form_notifier.html", "form_user.html", "form_checkin.html"}
templates := []string{"base.html", "head.html", "nav.html", "footer.html", "scripts.html", "form_service.html", "form_notifier.html", "form_user.html", "form_checkin.html", "form_message.html"}
javascripts := []string{"charts.js", "chart_index.js"}

View File

@ -16,6 +16,7 @@
package handlers
import (
"database/sql"
"encoding/json"
"github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/types"
@ -108,7 +109,7 @@ func DesktopInit(ip string, port int) {
Username: config.Username,
Password: config.Password,
Email: config.Email,
Admin: true,
Admin: sql.NullBool{true, true},
})
admin.Create()

147
handlers/messages.go Normal file
View File

@ -0,0 +1,147 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package handlers
import (
"fmt"
"github.com/gorilla/mux"
"github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"net/http"
"time"
)
func messagesHandler(w http.ResponseWriter, r *http.Request) {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
messages, _ := core.SelectMessages()
executeResponse(w, r, "messages.html", messages, nil)
}
func deleteMessageHandler(w http.ResponseWriter, r *http.Request) {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
id := utils.StringInt(vars["id"])
message, err := core.SelectMessage(id)
if err != nil {
http.Redirect(w, r, "/messages", http.StatusSeeOther)
return
}
message.Delete()
http.Redirect(w, r, "/messages", http.StatusSeeOther)
}
func viewMessageHandler(w http.ResponseWriter, r *http.Request) {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
id := utils.StringInt(vars["id"])
message, err := core.SelectMessage(id)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
executeResponse(w, r, "message.html", message, nil)
}
func updateMessageHandler(w http.ResponseWriter, r *http.Request) {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
id := utils.StringInt(vars["id"])
message, err := core.SelectMessage(id)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
r.ParseForm()
title := r.PostForm.Get("title")
description := r.PostForm.Get("description")
notifyMethod := r.PostForm.Get("notify_method")
notifyUsers := r.PostForm.Get("notify_users")
startOn := r.PostForm.Get("start_on")
endOn := r.PostForm.Get("end_on")
notifyBefore := r.PostForm.Get("notify_before")
serviceId := utils.StringInt(r.PostForm.Get("service_id"))
start, err := time.Parse(utils.FlatpickrTime, startOn)
if err != nil {
utils.Log(3, err)
}
end, _ := time.Parse(utils.FlatpickrTime, endOn)
before, _ := time.ParseDuration(notifyBefore)
message.Title = title
message.Description = description
message.NotifyUsers = utils.NullBool(notifyUsers == "on")
message.NotifyMethod = notifyMethod
message.StartOn = start.UTC()
message.EndOn = end.UTC()
message.NotifyBefore = before
message.ServiceId = serviceId
message.Update()
executeResponse(w, r, "messages.html", message, "/messages")
}
func createMessageHandler(w http.ResponseWriter, r *http.Request) {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
r.ParseForm()
title := r.PostForm.Get("title")
description := r.PostForm.Get("description")
notifyMethod := r.PostForm.Get("notify_method")
notifyUsers := r.PostForm.Get("notify_users")
startOn := r.PostForm.Get("start_on")
endOn := r.PostForm.Get("end_on")
notifyBefore := r.PostForm.Get("notify_before")
serviceId := utils.StringInt(r.PostForm.Get("service_id"))
start, _ := time.Parse(utils.FlatpickrTime, startOn)
end, _ := time.Parse(utils.FlatpickrTime, endOn)
before, _ := time.ParseDuration(notifyBefore)
message := core.ReturnMessage(&types.Message{
Title: title,
Description: description,
StartOn: start.UTC(),
EndOn: end.UTC(),
ServiceId: serviceId,
NotifyUsers: utils.NullBool(notifyUsers == "on"),
NotifyMethod: notifyMethod,
NotifyBefore: before,
})
_, err := message.Create()
if err != nil {
utils.Log(3, fmt.Sprintf("Error creating message %v", err))
}
messages, _ := core.SelectMessages()
executeResponse(w, r, "messages.html", messages, "/messages")
}

View File

@ -70,6 +70,13 @@ func Router() *mux.Router {
r.Handle("/user/{id}", http.HandlerFunc(updateUserHandler)).Methods("POST")
r.Handle("/user/{id}/delete", http.HandlerFunc(usersDeleteHandler)).Methods("GET")
// MESSAGES Routes
r.Handle("/messages", http.HandlerFunc(messagesHandler)).Methods("GET")
r.Handle("/messages", http.HandlerFunc(createMessageHandler)).Methods("POST")
r.Handle("/message/{id}", http.HandlerFunc(viewMessageHandler)).Methods("GET")
r.Handle("/message/{id}", http.HandlerFunc(updateMessageHandler)).Methods("POST")
r.Handle("/message/{id}/delete", http.HandlerFunc(deleteMessageHandler)).Methods("GET")
// SETTINGS Routes
r.Handle("/settings", http.HandlerFunc(settingsHandler)).Methods("GET")
r.Handle("/settings", http.HandlerFunc(saveSettingsHandler)).Methods("POST")

View File

@ -104,12 +104,12 @@ func createServiceHandler(w http.ResponseWriter, r *http.Request) {
Name: name,
Domain: domain,
Method: method,
Expected: expected,
Expected: utils.NullString(expected),
ExpectedStatus: status,
Interval: interval,
Type: checkType,
Port: port,
PostData: postData,
PostData: utils.NullString(postData),
Timeout: timeout,
Order: order,
})
@ -139,8 +139,11 @@ func servicesDeleteHandler(w http.ResponseWriter, r *http.Request) {
func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
fields := parseGet(r)
r.ParseForm()
startField := utils.StringInt(fields.Get("start"))
endField := utils.StringInt(fields.Get("end"))
group := r.Form.Get("group")
serv := core.SelectService(utils.StringInt(vars["id"]))
if serv == nil {
w.WriteHeader(http.StatusNotFound)
@ -156,15 +159,18 @@ func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
if endField != 0 {
end = time.Unix(endField, 0)
}
if group == "" {
group = "hour"
}
data := core.GraphDataRaw(serv, start, end, "hour", "latency")
data := core.GraphDataRaw(serv, start, end, group, "latency")
out := struct {
Service *core.Service
Start int64
End int64
Start string
End string
Data string
}{serv, start.Unix(), end.Unix(), data.ToString()}
}{serv, start.Format(utils.FlatpickrReadable), end.Format(utils.FlatpickrReadable), data.ToString()}
executeResponse(w, r, "service.html", out, nil)
}
@ -193,11 +199,11 @@ func servicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
service.Domain = domain
service.Method = method
service.ExpectedStatus = status
service.Expected = expected
service.Expected = utils.NullString(expected)
service.Interval = interval
service.Type = checkType
service.Port = port
service.PostData = postData
service.PostData = utils.NullString(postData)
service.Timeout = timeout
service.Order = order

View File

@ -56,8 +56,8 @@ func saveSettingsHandler(w http.ResponseWriter, r *http.Request) {
app.Style = style
}
footer := r.PostForm.Get("footer")
if footer != app.Footer {
app.Footer = footer
if footer != app.Footer.String {
app.Footer = utils.NullString(footer)
}
domain := r.PostForm.Get("domain")
if domain != app.Domain {
@ -67,7 +67,7 @@ func saveSettingsHandler(w http.ResponseWriter, r *http.Request) {
timeFloat, _ := strconv.ParseFloat(timezone, 10)
app.Timezone = float32(timeFloat)
app.UseCdn = (r.PostForm.Get("enable_cdn") == "on")
app.UseCdn = utils.NullBool(r.PostForm.Get("enable_cdn") == "on")
core.CoreApp, _ = core.UpdateCore(app)
//notifiers.OnSettingsSaved(core.CoreApp.ToCore())
executeResponse(w, r, "settings.html", core.CoreApp, "/settings")

View File

@ -16,6 +16,7 @@
package handlers
import (
"database/sql"
"github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
@ -113,7 +114,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
Username: config.Username,
Password: config.Password,
Email: config.Email,
Admin: true,
Admin: sql.NullBool{true, true},
})
admin.Create()

View File

@ -16,6 +16,7 @@
package handlers
import (
"database/sql"
"fmt"
"github.com/gorilla/mux"
"github.com/hunterlong/statup/core"
@ -52,8 +53,8 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
}
r.ParseForm()
vars := mux.Vars(r)
id, _ := strconv.Atoi(vars["id"])
user, err := core.SelectUser(int64(id))
id := utils.StringInt(vars["id"])
user, err := core.SelectUser(id)
if err != nil {
utils.Log(3, fmt.Sprintf("user error: %v", err))
w.WriteHeader(http.StatusInternalServerError)
@ -62,12 +63,21 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
user.Username = r.PostForm.Get("username")
user.Email = r.PostForm.Get("email")
user.Admin = (r.PostForm.Get("admin") == "on")
isAdmin := r.PostForm.Get("admin") == "on"
user.Admin = sql.NullBool{isAdmin, true}
password := r.PostForm.Get("password")
if password != "##########" {
user.Password = utils.HashPassword(password)
}
user.Update()
err = user.Update()
if err != nil {
utils.Log(3, err)
}
fmt.Println(user.Id)
fmt.Println(user.Password)
fmt.Println(user.Admin.Bool)
users, _ := core.SelectAllUsers()
executeResponse(w, r, "users.html", users, "/users")
}
@ -87,7 +97,7 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
Username: username,
Password: password,
Email: email,
Admin: (admin == "on"),
Admin: sql.NullBool{admin == "on", true},
})
_, err := user.Create()
if err != nil {

View File

@ -246,7 +246,7 @@ func (u *email) OnTest() error {
Method: "GET",
Timeout: 20,
LastStatusCode: 200,
Expected: "test example",
Expected: utils.NullString("test example"),
LastResponse: "<html>this is an example response</html>",
CreatedAt: time.Now().Add(-24 * time.Hour),
}

View File

@ -152,7 +152,7 @@ func (w *webhooker) OnTest() error {
Method: "GET",
Timeout: 20,
LastStatusCode: 404,
Expected: "test example",
Expected: utils.NullString("test example"),
LastResponse: "<html>this is an example response</html>",
CreatedAt: time.Now().Add(-24 * time.Hour),
}

View File

@ -1,4 +1,3 @@
@charset "UTF-8";
/* Index Page */
/* Status Container */
/* Button Colors */
@ -406,186 +405,6 @@ HTML, BODY {
.pointer {
cursor: pointer; }
/*!
* Pikaday
* Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/
*/
.pika-single {
z-index: 9999;
display: block;
position: relative;
color: #333;
background: #fff;
border: 1px solid #ccc;
border-bottom-color: #bbb;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
.pika-single.is-hidden {
display: none; }
.pika-single.is-bound {
position: absolute;
box-shadow: 0 5px 15px -5px rgba(0, 0, 0, 0.5); }
.pika-single {
*zoom: 1; }
.pika-single:before, .pika-single:after {
content: " ";
display: table; }
.pika-single:after {
clear: both; }
.pika-lendar {
float: left;
width: 240px;
margin: 8px; }
.pika-title {
position: relative;
text-align: center; }
.pika-title select {
cursor: pointer;
position: absolute;
z-index: 9998;
margin: 0;
left: 0;
top: 5px;
filter: alpha(opacity=0);
opacity: 0; }
.pika-label {
display: inline-block;
*display: inline;
position: relative;
z-index: 9999;
overflow: hidden;
margin: 0;
padding: 5px 3px;
font-size: 14px;
line-height: 20px;
font-weight: bold;
color: #333;
background-color: #fff; }
.pika-prev,
.pika-next {
display: block;
cursor: pointer;
position: relative;
outline: none;
border: 0;
padding: 0;
width: 20px;
height: 30px;
text-indent: 20px;
white-space: nowrap;
overflow: hidden;
background-color: transparent;
background-position: center center;
background-repeat: no-repeat;
background-size: 75% 75%;
opacity: .5;
*position: absolute;
*top: 0; }
.pika-prev:hover,
.pika-next:hover {
opacity: 1; }
.pika-prev.is-disabled,
.pika-next.is-disabled {
cursor: default;
opacity: .2; }
.pika-prev,
.is-rtl .pika-next {
float: left;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==");
*left: 0; }
.pika-next,
.is-rtl .pika-prev {
float: right;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=");
*right: 0; }
.pika-select {
display: inline-block;
*display: inline; }
.pika-table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
border: 0; }
.pika-table th,
.pika-table td {
width: 14.285714285714286%;
padding: 0; }
.pika-table th {
color: #999;
font-size: 12px;
line-height: 25px;
font-weight: bold;
text-align: center; }
.pika-table abbr {
border-bottom: none;
cursor: help; }
.pika-button {
cursor: pointer;
display: block;
-moz-box-sizing: border-box;
box-sizing: border-box;
outline: none;
border: 0;
margin: 0;
width: 100%;
padding: 5px;
color: #666;
font-size: 12px;
line-height: 15px;
text-align: right;
background: #f5f5f5; }
.is-today .pika-button {
color: #33aaff;
font-weight: bold; }
.is-selected .pika-button {
color: #fff;
font-weight: bold;
background: #33aaff;
box-shadow: inset 0 1px 3px #178fe5;
border-radius: 3px; }
.is-disabled .pika-button, .is-outside-current-month .pika-button {
color: #999;
opacity: .3; }
.is-disabled .pika-button {
pointer-events: none;
cursor: default; }
.pika-button:hover {
color: #fff;
background: #ff8000;
box-shadow: none;
border-radius: 3px; }
.pika-button .is-selection-disabled {
pointer-events: none;
cursor: default; }
.pika-week {
font-size: 11px;
color: #999; }
.is-inrange .pika-button {
background: #D5E9F7; }
.is-startrange .pika-button {
color: #fff;
background: #6CB31D;
box-shadow: none;
border-radius: 3px; }
.is-endrange .pika-button {
color: #fff;
background: #33aaff;
box-shadow: none;
border-radius: 3px; }
@media (max-width: 767px) {
HTML, BODY {
background-color: #fcfcfc; }

13
source/css/flatpickr.min.css vendored Normal file

File diff suppressed because one or more lines are too long

2
source/js/flatpickr.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

147
source/js/rangePlugin.js Normal file
View File

@ -0,0 +1,147 @@
/* flatpickr v4.5.2, @license MIT */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.rangePlugin = factory());
}(this, (function () { 'use strict';
function rangePlugin(config) {
if (config === void 0) {
config = {};
}
return function (fp) {
var dateFormat = "",
secondInput,
_secondInputFocused,
_prevDates;
var createSecondInput = function createSecondInput() {
if (config.input) {
secondInput = config.input instanceof Element ? config.input : window.document.querySelector(config.input);
} else {
secondInput = fp._input.cloneNode();
secondInput.removeAttribute("id");
secondInput._flatpickr = undefined;
}
if (secondInput.value) {
var parsedDate = fp.parseDate(secondInput.value);
if (parsedDate) fp.selectedDates.push(parsedDate);
}
secondInput.setAttribute("data-fp-omit", "");
fp._bind(secondInput, ["focus", "click"], function () {
if (fp.selectedDates[1]) {
fp.latestSelectedDateObj = fp.selectedDates[1];
fp._setHoursFromDate(fp.selectedDates[1]);
fp.jumpToDate(fp.selectedDates[1]);
}
_secondInputFocused = true;
fp.isOpen = false;
fp.open(undefined, secondInput);
});
fp._bind(fp._input, ["focus", "click"], function (e) {
e.preventDefault();
fp.isOpen = false;
fp.open();
});
if (fp.config.allowInput) fp._bind(secondInput, "keydown", function (e) {
if (e.key === "Enter") {
fp.setDate([fp.selectedDates[0], secondInput.value], true, dateFormat);
secondInput.click();
}
});
if (!config.input) fp._input.parentNode && fp._input.parentNode.insertBefore(secondInput, fp._input.nextSibling);
};
var plugin = {
onParseConfig: function onParseConfig() {
fp.config.mode = "range";
dateFormat = fp.config.altInput ? fp.config.altFormat : fp.config.dateFormat;
},
onReady: function onReady() {
createSecondInput();
fp.config.ignoredFocusElements.push(secondInput);
if (fp.config.allowInput) {
fp._input.removeAttribute("readonly");
secondInput.removeAttribute("readonly");
} else {
secondInput.setAttribute("readonly", "readonly");
}
fp._bind(fp._input, "focus", function () {
fp.latestSelectedDateObj = fp.selectedDates[0];
fp._setHoursFromDate(fp.selectedDates[0]);
_secondInputFocused = false;
fp.jumpToDate(fp.selectedDates[0]);
});
if (fp.config.allowInput) fp._bind(fp._input, "keydown", function (e) {
if (e.key === "Enter") fp.setDate([fp._input.value, fp.selectedDates[1]], true, dateFormat);
});
fp.setDate(fp.selectedDates, false);
plugin.onValueUpdate(fp.selectedDates);
},
onPreCalendarPosition: function onPreCalendarPosition() {
if (_secondInputFocused) {
fp._positionElement = secondInput;
setTimeout(function () {
fp._positionElement = fp._input;
}, 0);
}
},
onChange: function onChange() {
if (!fp.selectedDates.length) {
setTimeout(function () {
if (fp.selectedDates.length) return;
secondInput.value = "";
_prevDates = [];
}, 10);
}
if (_secondInputFocused) {
setTimeout(function () {
secondInput.focus();
}, 0);
}
},
onDestroy: function onDestroy() {
if (!config.input) secondInput.parentNode && secondInput.parentNode.removeChild(secondInput);
},
onValueUpdate: function onValueUpdate(selDates) {
if (!secondInput) return;
_prevDates = !_prevDates || selDates.length >= _prevDates.length ? selDates.concat() : _prevDates;
if (_prevDates.length > selDates.length) {
var newSelectedDate = selDates[0];
var newDates = _secondInputFocused ? [_prevDates[0], newSelectedDate] : [newSelectedDate, _prevDates[1]];
fp.setDate(newDates, false);
_prevDates = newDates.concat();
}
var _fp$selectedDates$map = fp.selectedDates.map(function (d) {
return fp.formatDate(d, dateFormat);
});
var _fp$selectedDates$map2 = _fp$selectedDates$map[0];
fp._input.value = _fp$selectedDates$map2 === void 0 ? "" : _fp$selectedDates$map2;
var _fp$selectedDates$map3 = _fp$selectedDates$map[1];
secondInput.value = _fp$selectedDates$map3 === void 0 ? "" : _fp$selectedDates$map3;
}
};
return plugin;
};
}
return rangePlugin;
})));

View File

@ -468,6 +468,4 @@ HTML,BODY {
cursor: pointer;
}
@import './pikaday';
@import './mobile';

View File

@ -1,7 +1,7 @@
{{ define "footer"}}
<div class="footer text-center mb-4">
{{ if CoreApp.Footer}}
{{ CoreApp.Footer }}
{{ if CoreApp.Footer.String }}
{{ CoreApp.Footer.String }}
{{ else }}
<a href="https://github.com/hunterlong/statup" target="_blank">Statup {{VERSION}} made with <i class="text-danger fas fa-heart"></i></a> | <a href="/dashboard">Dashboard</a>
{{ end }}

View File

@ -0,0 +1,61 @@
{{define "form_message"}}
{{$message := .}}
<form action="{{if ne .Id 0}}/message/{{.Id}}{{else}}/messages{{end}}" method="POST">
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">
<input type="text" name="title" class="form-control" value="{{.Title}}" id="title" placeholder="Message Title" required>
</div>
</div>
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Description</label>
<div class="col-sm-8">
<textarea rows="5" name="description" class="form-control" id="description" required>{{.Description}}</textarea>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">Message Date Range</label>
<div class="col-sm-4">
<input type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="{{.StartOn}}" required>
</div>
<div class="col-sm-4">
<input type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="{{.EndOn}}" required>
</div>
</div>
<div class="form-group row">
<label for="service_id" class="col-sm-4 col-form-label">Service</label>
<div class="col-sm-8">
<select class="form-control" name="service_id" id="service_id">
<option value="0" {{if eq (ToString .ServiceId) "0"}}selected{{end}}>Global Message</option>
{{range Services}}
{{$s := .Select}}
<option value="{{$s.Id}}" {{if eq $message.ServiceId $s.Id}}selected{{end}}>{{$s.Name}}</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row">
<label for="notify_method" class="col-sm-4 col-form-label">Notification Method</label>
<div class="col-sm-8">
<input type="text" name="notify_method" class="form-control" id="notify_method" value="{{.NotifyUsers.Bool}}" placeholder="email">
</div>
</div>
<div class="form-group row">
<label for="notify_before" class="col-sm-4 col-form-label">Notify Before</label>
<div class="col-sm-8">
<input type="text" name="notify_before" class="form-control" id="notify_before" value="{{.NotifyBefore}}">
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update Message{{else}}Create Message{{end}}</button>
</div>
</div>
</form>
{{end}}

View File

@ -41,14 +41,14 @@
<div class="form-group row{{if ne .Method "POST"}} d-none{{end}}">
<label for="post_data" class="col-sm-4 col-form-label">Optional Post Data (JSON)</label>
<div class="col-sm-8">
<textarea name="post_data" class="form-control" id="post_data" rows="3" autocapitalize="false" spellcheck="false" placeholder='{"data": { "method": "success", "id": 148923 } }'>{{.PostData}}</textarea>
<textarea name="post_data" class="form-control" id="post_data" rows="3" autocapitalize="false" spellcheck="false" placeholder='{"data": { "method": "success", "id": 148923 } }'>{{.PostData.String}}</textarea>
<small class="form-text text-muted">Insert a JSON string to send data to the endpoint.</small>
</div>
</div>
<div class="form-group row{{if (eq .Type "tcp") or (eq .Type "udp")}} d-none{{end}}">
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8">
<textarea name="expected" class="form-control" id="service_response" rows="3" autocapitalize="false" spellcheck="false" placeholder='(method)": "((\\"|[success])*)"'>{{.Expected}}</textarea>
<textarea name="expected" class="form-control" id="service_response" rows="3" autocapitalize="false" spellcheck="false" placeholder='(method)": "((\\"|[success])*)"'>{{.Expected.String}}</textarea>
<small class="form-text text-muted">You can use plain text or insert <a target="_blank" href="https://regex101.com/r/I5bbj9/1">Regex</a> to validate the response</small>
</div>
</div>
@ -60,16 +60,16 @@
</div>
</div>
<div class="form-group row{{if (ne .Type "tcp") and (ne .Type "udp")}} d-none{{end}}">
<label id="service_type_label" for="service_port" class="col-sm-4 col-form-label">TCP Port</label>
<label for="port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8">
<input type="number" name="port" class="form-control" value="{{if ne .Port 0}}{{.Port}}{{end}}" id="service_port" placeholder="8080">
</div>
</div>
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<label for="interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8">
<input type="number" name="interval" class="form-control" value="{{if ne .Interval 0}}{{.Interval}}{{else}}60{{end}}" min="1" id="service_interval" required>
<small id="emailHelp" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
<small id="interval" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
</div>
</div>
<div class="form-group row">

View File

@ -7,7 +7,7 @@
</div>
<div class="col-6 col-md-4">
<span class="switch">
<input type="checkbox" name="admin" class="switch" id="switch-normal"{{if .Admin}} checked{{end}}>
<input type="checkbox" name="admin" class="switch" id="switch-normal"{{if .Admin.Bool}} checked{{end}}>
<label for="switch-normal">Administrator</label>
</span>
</div>

28
source/tmpl/message.html Normal file
View File

@ -0,0 +1,28 @@
{{define "title"}}Statup | {{.Title}}{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{template "nav"}}
<div class="col-12">
<h3>Message {{.Title}}</h3>
{{template "form_message" .}}
</div>
</div>
{{end}}
{{define "extra_css"}}
<link rel="stylesheet" href="/css/flatpickr.min.css">
{{end}}
{{define "extra_scripts"}}
<script src="/js/flatpickr.js"></script>
<script src="/js/rangePlugin.js"></script>
<script>
$(document).ready(function() {
$("#start_on").flatpickr({
enableTime: true,
dateFormat: "Y-m-d H:i",
minDate: "today",
"plugins": [new rangePlugin({ input: "#end_on"})]
});
});
</script>
{{end}}

52
source/tmpl/messages.html Normal file
View File

@ -0,0 +1,52 @@
{{define "title"}}Statup Messages{{end}}
{{define "content"}}
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
{{template "nav"}}
<div class="col-12">
<h3>Messages</h3>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{{range .}}
<tr>
<td>{{.Title}}</td>
<td class="text-right" id="message_{{.Id}}">
<div class="btn-group">
<a href="/message/{{.Id}}" class="btn btn-outline-secondary"><i class="fas fa-exclamation-triangle"></i> Edit</a>
<a href="/message/{{.Id}}/delete" class="btn btn-danger confirm-btn" data-id="message_{{.Id}}"><i class="fas fa-times"></i></a>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
<h3>Create Message</h3>
{{template "form_message" NewMessage}}
</div>
</div>
{{end}}
{{define "extra_css"}}
<link rel="stylesheet" href="/css/flatpickr.min.css">
{{end}}
{{define "extra_scripts"}}
<script src="/js/flatpickr.js"></script>
<script src="/js/rangePlugin.js"></script>
<script>
$(document).ready(function() {
$("#start_on").flatpickr({
enableTime: true,
dateFormat: "Y-m-d H:i",
minDate: "today",
"plugins": [new rangePlugin({ input: "#end_on"})]
});
});
</script>
{{end}}

View File

@ -16,6 +16,9 @@
<li class="nav-item{{ if eq URL "/users" }} active{{ end }}">
<a class="nav-link" href="/users">Users</a>
</li>
<li class="nav-item{{ if eq URL "/messages" }} active{{ end }}">
<a class="nav-link" href="/messages">Messages</a>
</li>
<li class="nav-item{{ if eq URL "/settings" }} active{{ end }}">
<a class="nav-link" href="/settings">Settings</a>
</li>

View File

@ -10,11 +10,11 @@
<div class="col-12 mb-4">
{{if $s.Online }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span>
{{ else }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-danger d-block d-md-none">OFFLINE</span>
{{end}}
{{if $s.Online }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span>
{{ else }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-danger d-block d-md-none">OFFLINE</span>
{{end}}
<h4 class="mt-2">{{ $s.Name }}
{{if $s.Online }}
@ -46,11 +46,11 @@
</div>
<form id="service_date_form" class="col-12 mt-2 mb-3">
<span id="start_date" class="text-muted small float-left pointer">{{FromUnix .Start}}</span>
<span id="end_date" class="text-muted small float-right pointer" style="position: absolute;right: 0;">{{FromUnix .End}}</span>
<input type="hidden" name="start" class="form-control" id="service_start" spellcheck="false">
<input type="hidden" name="end" class="form-control" id="service_end" spellcheck="false">
<button type="submit" class="btn btn-light btn-block btn-sm mt-2">Set Timeframe</button>
<input type="text" class="d-none" name="start" id="service_start" data-input>
<span data-toggle title="toggle" id="start_date" class="text-muted small float-left pointer mt-2">{{.Start}} to {{.End}}</span>
<button type="submit" class="btn btn-light btn-sm mt-2">Set Timeframe</button>
<input type="text" class="d-none" name="end" id="service_end" data-input>
<div id="start_container"></div>
<div id="end_container"></div>
</form>
@ -59,6 +59,15 @@
<div class="col-12 small text-center mt-3 text-muted">{{$s.DowntimeText}}</div>
{{end}}
{{if $s.Messages}}
{{range $s.Messages}}
<div class="alert alert-warning" role="alert">
<h3>{{.Title}}</h3>
<span>{{safe .Description}}</span>
</div>
{{end}}
{{end}}
{{ if $s.LimitedFailures }}
<div class="list-group mt-3 mb-4">
{{ range $s.LimitedFailures }}
@ -137,14 +146,15 @@
{{end}}
</div>
{{end}}
{{define "extra_scripts"}}
{{if USE_CDN}}
<script src="https://assets.statup.io/pikaday.js"></script>
{{ else }}
<script src="/js/pikaday.js"></script>
{{define "extra_css"}}
<link rel="stylesheet" href="/css/flatpickr.min.css">
{{end}}
{{define "extra_scripts"}}
{{$s := .Service}}
<script src="/js/flatpickr.js"></script>
<script src="/js/rangePlugin.js"></script>
<script>
$(document).ready(function() {
var ctx = document.getElementById("service").getContext('2d');
@ -208,45 +218,29 @@
}
});
var startPick = new Pikaday({
field: $('#service_start')[0],
bound: false,
trigger: $("#start_date"),
container: $("#start_container")[0],
maxDate: new Date(),
onSelect: function(date) {
$('#service_start')[0].value = Math.round(date.getTime() / 1000);
this.hide();
}
});
var endPick = new Pikaday({
field: $('#service_end')[0],
bound: false,
trigger: $("#end_date"),
container: $("#end_container")[0],
maxDate: new Date(),
onSelect: function(date) {
$('#service_end')[0].value = Math.round(date.getTime() / 1000);
this.hide();
}
});
startPick.setDate(new Date({{.Start}}* 1000));
endPick.setDate(new Date({{.End}}* 1000));
startPick.hide();
endPick.hide();
$("#start_date").click(function(e) {
startPick.show()
});
$("#end_date").click(function(e) {
endPick.show()
});
AjaxChart(chartdata,{{$s.Id}},{{.Start}},{{.End}},"hour");
let startDate = $("#service_start").flatpickr({
enableTime: false,
static: true,
altInput: true,
altFormat: "U",
maxDate: "today",
dateFormat: "F j, Y",
onChange: function(selectedDates, dateStr, instance) {
var one = Math.round((new Date(selectedDates[0])).getTime() / 1000);
var two = Math.round((new Date(selectedDates[1])).getTime() / 1000);
$("#service_start").val(one);
$("#service_end").val(two);
$("#start_date").html(dateStr);
},
"plugins": [new rangePlugin({ input: "#service_end"})]
});
$("#start_date").click(function(e) {
startDate.open()
});
});
</script>
{{end}}

View File

@ -54,7 +54,7 @@
<div class="form-group">
<label for="footer">Custom Footer</label>
<textarea rows="4" name="footer" class="form-control" id="footer">{{ .Footer }}</textarea>
<textarea rows="4" name="footer" class="form-control" id="footer">{{ .Footer.String }}</textarea>
</div>
<div class="form-group">

View File

@ -16,6 +16,7 @@
package types
import (
"database/sql"
"time"
)
@ -32,11 +33,11 @@ type Core struct {
ApiKey string `gorm:"column:api_key" json:"-"`
ApiSecret string `gorm:"column:api_secret" json:"-"`
Style string `gorm:"not null;column:style" json:"style,omitempty"`
Footer string `gorm:"not null;column:footer" json:"footer,omitempty"`
Domain string `gorm:"not null;column:domain" json:"domain,omitempty"`
Footer sql.NullString `gorm:"not null;column:footer" json:"footer"`
Domain string `gorm:"not null;column:domain" json:"domain"`
Version string `gorm:"column:version" json:"version"`
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`
UseCdn bool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
UseCdn sql.NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`

36
types/message.go Normal file
View File

@ -0,0 +1,36 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package types
import (
"database/sql"
"time"
)
// Message is for creating Announcements, Alerts and other messages for the end users
type Message struct {
Id int64 `gorm:"primary_key;column:id"`
Title string `gorm:"column:title"`
Description string `gorm:"column:description"`
StartOn time.Time `gorm:"column:start_on"`
EndOn time.Time `gorm:"column:end_on"`
ServiceId int64 `gorm:"index;column:service"`
NotifyUsers sql.NullBool `gorm:"column:notify_users"`
NotifyMethod string `gorm:"column:notify_method"`
NotifyBefore time.Duration `gorm:"column:notify_before"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
}

View File

@ -16,37 +16,39 @@
package types
import (
"database/sql"
"time"
)
// Service is the main struct for Services
type Service struct {
Id int64 `gorm:"primary_key;column:id" json:"id"`
Name string `gorm:"column:name" json:"name"`
Domain string `gorm:"column:domain" json:"domain"`
Expected string `gorm:"not null;column:expected" json:"expected"`
ExpectedStatus int `gorm:"default:200;column:expected_status" json:"expected_status"`
Interval int `gorm:"default:30;column:check_interval" json:"check_interval"`
Type string `gorm:"column:check_type" json:"type"`
Method string `gorm:"column:method" json:"method"`
PostData string `gorm:"not null;column:post_data" json:"post_data"`
Port int `gorm:"not null;column:port" json:"port"`
Timeout int `gorm:"default:30;column:timeout" json:"timeout"`
Order int `gorm:"default:0;column:order_id" json:"order_id"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Online bool `gorm:"-" json:"online"`
Latency float64 `gorm:"-" json:"latency"`
PingTime float64 `gorm:"-" json:"ping_time"`
Online24Hours float32 `gorm:"-" json:"online_24_hours"`
AvgResponse string `gorm:"-" json:"avg_response"`
Running chan bool `gorm:"-" json:"-"`
Checkpoint time.Time `gorm:"-" json:"-"`
SleepDuration time.Duration `gorm:"-" json:"-"`
LastResponse string `gorm:"-" json:"-"`
LastStatusCode int `gorm:"-" json:"status_code"`
LastOnline time.Time `gorm:"-" json:"last_online"`
Failures []interface{} `gorm:"-" json:"failures,omitempty"`
Id int64 `gorm:"primary_key;column:id" json:"id"`
Name string `gorm:"column:name" json:"name"`
Domain string `gorm:"column:domain" json:"domain"`
Expected sql.NullString `gorm:"not null;column:expected" json:"expected"`
ExpectedStatus int `gorm:"default:200;column:expected_status" json:"expected_status"`
Interval int `gorm:"default:30;column:check_interval" json:"check_interval"`
Type string `gorm:"column:check_type" json:"type"`
Method string `gorm:"column:method" json:"method"`
PostData sql.NullString `gorm:"not null;column:post_data" json:"post_data"`
Port int `gorm:"not null;column:port" json:"port"`
Timeout int `gorm:"default:30;column:timeout" json:"timeout"`
Order int `gorm:"default:0;column:order_id" json:"order_id"`
AllowNotifications sql.NullBool `gorm:"default:false;column:allow_notifications" json:"allow_notifications"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Online bool `gorm:"-" json:"online"`
Latency float64 `gorm:"-" json:"latency"`
PingTime float64 `gorm:"-" json:"ping_time"`
Online24Hours float32 `gorm:"-" json:"online_24_hours"`
AvgResponse string `gorm:"-" json:"avg_response"`
Running chan bool `gorm:"-" json:"-"`
Checkpoint time.Time `gorm:"-" json:"-"`
SleepDuration time.Duration `gorm:"-" json:"-"`
LastResponse string `gorm:"-" json:"-"`
LastStatusCode int `gorm:"-" json:"status_code"`
LastOnline time.Time `gorm:"-" json:"last_online"`
Failures []interface{} `gorm:"-" json:"failures,omitempty"`
}
type ServiceInterface interface {

View File

@ -16,20 +16,21 @@
package types
import (
"database/sql"
"time"
)
// User is the main struct for Users
type User struct {
Id int64 `gorm:"primary_key;column:id" json:"id"`
Username string `gorm:"type:varchar(100);unique;column:username;" json:"username"`
Password string `gorm:"column:password" json:"-"`
Email string `gorm:"type:varchar(100);unique;column:email" json:"-"`
ApiKey string `gorm:"column:api_key" json:"api_key"`
ApiSecret string `gorm:"column:api_secret" json:"-"`
Admin bool `gorm:"column:administrator" json:"admin"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Id int64 `gorm:"primary_key;column:id" json:"id"`
Username string `gorm:"type:varchar(100);unique;column:username;" json:"username"`
Password string `gorm:"column:password" json:"-"`
Email string `gorm:"type:varchar(100);unique;column:email" json:"-"`
ApiKey string `gorm:"column:api_key" json:"api_key"`
ApiSecret string `gorm:"column:api_secret" json:"-"`
Admin sql.NullBool `gorm:"column:administrator" json:"admin"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
UserInterface `gorm:"-" json:"-"`
}

View File

@ -20,6 +20,12 @@ import (
"time"
)
const (
FlatpickrTime = "2006-01-02 15:04"
FlatpickrDay = "2006-01-02"
FlatpickrReadable = "Mon, 02 Jan 2006"
)
// FormatDuration converts a time.Duration into a string
func FormatDuration(d time.Duration) string {
var out string

View File

@ -16,6 +16,7 @@
package utils
import (
"database/sql"
"errors"
"fmt"
"github.com/ararog/timeago"
@ -43,6 +44,20 @@ func init() {
}
}
func NullString(s string) sql.NullString {
return sql.NullString{s, true}
}
func NullBool(s bool) sql.NullBool {
return sql.NullBool{s, true}
}
func StringPoint(s string) *string {
val := new(string)
*val = s
return val
}
// StringInt converts a string to an int64
func StringInt(s string) int64 {
num, _ := strconv.Atoi(s)