core updates - slack/email queue

pull/10/head v0.28.6
Hunter Long 2018-07-01 23:21:41 -07:00
parent b741e55e35
commit 70150c5318
28 changed files with 664 additions and 276 deletions

View File

@ -18,7 +18,7 @@ services:
env: env:
global: global:
- VERSION=0.28.5 - VERSION=0.28.6
- DB_HOST=localhost - DB_HOST=localhost
- DB_USER=travis - DB_USER=travis
- DB_PASS= - DB_PASS=

27
.travis/Dockerfile.dev Normal file
View File

@ -0,0 +1,27 @@
FROM golang:1.10.3-alpine
RUN apk update && apk add git g++ libstdc++ ca-certificates
WORKDIR $GOPATH/src/github.com/hunterlong/statup/
COPY . $GOPATH/src/github.com/hunterlong/statup/
RUN go get github.com/GeertJohan/go.rice/rice
RUN go get -d -v
RUN rice embed-go
RUN go install
RUN wget -q https://assets.statup.io/sass && \
chmod +x sass && \
mv sass /usr/local/bin/sass
ENV IS_DOCKER=true
ENV SASS=/usr/local/bin/sass
ENV CMD_FILE=/usr/bin/cmd
RUN printf "#!/usr/bin/env sh\n\$1\n" > $CMD_FILE && \
chmod +x $CMD_FILE
WORKDIR /app
VOLUME /app
EXPOSE 8080
CMD ["/go/bin/statup"]

8
.travis/docker.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
cd .travis
cp Dockerfile.dev ../
cd ../
docker build -t hunterlong/statup:dev -f Dockerfile.dev .
rm -rf Dockerfile.dev

View File

@ -1,6 +1,6 @@
FROM alpine:latest FROM alpine:latest
ENV VERSION=v0.28.5 ENV VERSION=v0.28.6
RUN apk --no-cache add libstdc++ ca-certificates RUN apk --no-cache add libstdc++ ca-certificates
RUN wget -q https://github.com/hunterlong/statup/releases/download/$VERSION/statup-linux-alpine.tar.gz && \ RUN wget -q https://github.com/hunterlong/statup/releases/download/$VERSION/statup-linux-alpine.tar.gz && \

View File

@ -95,7 +95,7 @@ func CreateAllAssets() {
utils.Log(1, "Inserting scss, css, emails, and javascript files into assets..") utils.Log(1, "Inserting scss, css, emails, and javascript files into assets..")
CopyToPublic(ScssBox, "scss", "base.scss") CopyToPublic(ScssBox, "scss", "base.scss")
CopyToPublic(ScssBox, "scss", "variables.scss") CopyToPublic(ScssBox, "scss", "variables.scss")
CopyToPublic(EmailBox, "emails", "error.html") CopyToPublic(EmailBox, "emails", "message.html")
CopyToPublic(EmailBox, "emails", "failure.html") CopyToPublic(EmailBox, "emails", "failure.html")
CopyToPublic(CssBox, "css", "bootstrap.min.css") CopyToPublic(CssBox, "css", "bootstrap.min.css")
CopyToPublic(JsBox, "js", "bootstrap.min.js") CopyToPublic(JsBox, "js", "bootstrap.min.js")
@ -106,7 +106,6 @@ func CreateAllAssets() {
CopyToPublic(JsBox, "js", "setup.js") CopyToPublic(JsBox, "js", "setup.js")
CopyToPublic(TmplBox, "", "robots.txt") CopyToPublic(TmplBox, "", "robots.txt")
CopyToPublic(TmplBox, "", "favicon.ico") CopyToPublic(TmplBox, "", "favicon.ico")
utils.Log(1, "Compiling CSS from SCSS style...") utils.Log(1, "Compiling CSS from SCSS style...")
err := CompileSASS() err := CompileSASS()
if err != nil { if err != nil {

View File

@ -17,10 +17,10 @@ type FailureData types.FailureData
func CheckServices() { func CheckServices() {
CoreApp.Services, _ = SelectAllServices() CoreApp.Services, _ = SelectAllServices()
utils.Log(1, fmt.Sprintf("Loaded %v Services", len(CoreApp.Services))) utils.Log(1, fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.Services)))
for _, v := range CoreApp.Services { for _, v := range CoreApp.Services {
obj := v obj := v
go obj.StartCheckins() //go obj.StartCheckins()
go obj.CheckQueue() go obj.CheckQueue()
} }
} }

View File

@ -1,21 +1,36 @@
package core package core
import ( import (
"bytes"
"fmt" "fmt"
"github.com/hunterlong/statup/notifications"
"github.com/hunterlong/statup/types" "github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"net/http"
"time" "time"
) )
type Communication types.Communication
func LoadDefaultCommunications() { func LoadDefaultCommunications() {
emailer := SelectCommunication(1) notifications.EmailComm = SelectCommunication(1)
emailer := notifications.EmailComm
if emailer.Enabled { if emailer.Enabled {
//LoadMailer(emailer) admin, _ := SelectUser(1)
//go EmailerQueue() notifications.LoadEmailer(emailer)
email := &types.Email{
To: admin.Email,
Subject: "Test Email",
Template: "message.html",
Data: nil,
From: emailer.Var1,
}
notifications.SendEmail(EmailBox, email)
go notifications.EmailRoutine()
}
notifications.SlackComm = SelectCommunication(2)
slack := notifications.SlackComm
if slack.Enabled {
notifications.LoadSlack(slack.Host)
msg := fmt.Sprintf("Slack loaded on your Statup Status Page!")
notifications.SendSlack(msg)
go notifications.SlackRoutine()
} }
} }
@ -27,25 +42,15 @@ func LoadComms() {
} }
} }
func Run(c *Communication) { func SelectAllCommunications() ([]*types.Communication, error) {
var c []*types.Communication
//sample := &Email{
// To: "info@socialeck.com",
// Subject: "Test Email from Statup",
//}
//AddEmail(sample)
}
func SelectAllCommunications() ([]*Communication, error) {
var c []*Communication
col := DbSession.Collection("communication").Find() col := DbSession.Collection("communication").Find()
err := col.All(&c) err := col.OrderBy("id").All(&c)
CoreApp.Communications = c CoreApp.Communications = c
return c, err return c, err
} }
func Create(c *Communication) (int64, error) { func Create(c *types.Communication) (int64, error) {
c.CreatedAt = time.Now() c.CreatedAt = time.Now()
uuid, err := DbSession.Collection("communication").Insert(c) uuid, err := DbSession.Collection("communication").Insert(c)
if err != nil { if err != nil {
@ -62,45 +67,30 @@ func Create(c *Communication) (int64, error) {
return uuid.(int64), err return uuid.(int64), err
} }
func Disable(c *Communication) { func Disable(c *types.Communication) {
c.Enabled = false c.Enabled = false
Update(c) Update(c)
} }
func Enable(c *Communication) { func Enable(c *types.Communication) {
c.Enabled = true c.Enabled = true
Update(c) Update(c)
} }
func Update(c *Communication) *Communication { func Update(c *types.Communication) *types.Communication {
col := DbSession.Collection("communication").Find("id", c.Id) col := DbSession.Collection("communication").Find("id", c.Id)
col.Update(c) col.Update(c)
SelectAllCommunications()
return c return c
} }
func SelectCommunication(id int64) *Communication { func SelectCommunication(id int64) *types.Communication {
for _, c := range CoreApp.Communications { var comm *types.Communication
if c.Id == id { col := DbSession.Collection("communication").Find("id", id)
return c err := col.One(&comm)
} if err != nil {
} utils.Log(2, err)
return nil
}
func SendSlackMessage(msg string) error {
fullMessage := fmt.Sprintf("{\"text\":\"%v\"}", msg)
utils.Log(1, fmt.Sprintf("Sending JSON to Slack Webhook: %v", fullMessage))
slack := SelectCommunication(2)
if slack == nil {
utils.Log(3, fmt.Sprintf("Slack communication database entry was not found."))
return nil return nil
} }
client := http.Client{ return comm
Timeout: 15 * time.Second,
}
_, err := client.Post(slack.Host, "application/json", bytes.NewBuffer([]byte(fullMessage)))
if err != nil {
utils.Log(3, err)
}
return err
} }

View File

@ -23,7 +23,7 @@ type Core struct {
Plugins []plugin.Info Plugins []plugin.Info
Repos []PluginJSON Repos []PluginJSON
//PluginFields []PluginSelect //PluginFields []PluginSelect
Communications []*Communication Communications []*types.Communication
OfflineAssets bool OfflineAssets bool
} }
@ -45,6 +45,16 @@ func init() {
CoreApp = new(Core) CoreApp = new(Core)
} }
func InitApp() {
SelectCore()
SelectAllCommunications()
InsertDefaultComms()
LoadDefaultCommunications()
SelectAllServices()
CheckServices()
go DatabaseMaintence()
}
func (c *Core) Update() (*Core, error) { func (c *Core) Update() (*Core, error) {
res := DbSession.Collection("core").Find().Limit(1) res := DbSession.Collection("core").Find().Limit(1)
res.Update(c) res.Update(c)

View File

@ -3,7 +3,9 @@ package core
import ( import (
"fmt" "fmt"
"github.com/fatih/structs" "github.com/fatih/structs"
"github.com/hunterlong/statup/notifications"
"github.com/hunterlong/statup/plugin" "github.com/hunterlong/statup/plugin"
"github.com/hunterlong/statup/types"
"upper.io/db.v3/lib/sqlbuilder" "upper.io/db.v3/lib/sqlbuilder"
) )
@ -23,13 +25,38 @@ func OnFailure(s *Service, f FailureData) {
for _, p := range AllPlugins { for _, p := range AllPlugins {
p.OnFailure(structs.Map(s)) p.OnFailure(structs.Map(s))
} }
onFailureEmail(s, f)
onFailureSlack(s, f)
}
func onFailureSlack(s *Service, f FailureData) {
slack := SelectCommunication(2) slack := SelectCommunication(2)
if slack == nil {
return
}
if slack.Enabled { if slack.Enabled {
msg := fmt.Sprintf("Service %v is currently offline! Issue: %v", s.Name, f.Issue) msg := fmt.Sprintf("Service %v is currently offline! Issue: %v", s.Name, f.Issue)
SendSlackMessage(msg) notifications.SendSlack(msg)
}
}
type failedEmail struct {
Service *Service
FailureData FailureData
Domain string
}
func onFailureEmail(s *Service, f FailureData) {
email := SelectCommunication(1)
if email.Enabled {
data := failedEmail{s, f, CoreApp.Domain}
admin, _ := SelectUser(1)
email := &types.Email{
To: admin.Email,
Subject: fmt.Sprintf("Service %v is Down", s.Name),
Template: "failure.html",
Data: data,
From: email.Var1,
}
notifications.SendEmail(EmailBox, email)
} }
} }

View File

@ -1,23 +1,30 @@
package core package core
import ( import (
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"os" "os"
) )
func InsertDefaultComms() { func InsertDefaultComms() {
emailer := &Communication{ emailer := SelectCommunication(1)
Method: "email", if emailer == nil {
Removable: false, emailer := &types.Communication{
Enabled: false, Method: "email",
Removable: false,
Enabled: false,
}
Create(emailer)
} }
Create(emailer) slack := SelectCommunication(2)
slack := &Communication{ if slack == nil {
Method: "slack", slack := &types.Communication{
Removable: false, Method: "slack",
Enabled: false, Removable: false,
Enabled: false,
}
Create(slack)
} }
Create(slack)
} }
func DeleteConfig() { func DeleteConfig() {
@ -82,12 +89,14 @@ func LoadSampleData() error {
} }
checkin.Create() checkin.Create()
for i := 0; i < 20; i++ { //for i := 0; i < 3; i++ {
s1.Check() // s1.Check()
s2.Check() // s2.Check()
s3.Check() // s3.Check()
s4.Check() // s4.Check()
} //}
utils.Log(1, "Sample data has finished importing")
return nil return nil
} }

View File

@ -1,114 +0,0 @@
package main
import (
"bytes"
"crypto/tls"
"fmt"
"github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"gopkg.in/gomail.v2"
"html/template"
"time"
)
var (
emailQue *Que
)
type Email types.Email
type Que struct {
Mailer *gomail.Dialer
Outgoing []*Email
LastSent int
LastSentTime time.Time
}
func AddEmail(email *Email) {
if emailQue == nil {
return
}
emailQue.Outgoing = append(emailQue.Outgoing, email)
}
func EmailerQueue() {
if emailQue == nil {
return
}
uniques := []*Email{}
for _, out := range emailQue.Outgoing {
if isUnique(uniques, out) {
msg := fmt.Sprintf("sending email to: %v \n", out.To)
Send(out)
utils.Log(0, msg)
uniques = append(uniques, out)
}
}
emailQue.Outgoing = nil
fmt.Println("running emailer queue")
time.Sleep(60 * time.Second)
EmailerQueue()
}
func isUnique(arr []*Email, obj *Email) bool {
for _, v := range arr {
if v.To == obj.To && v.Subject == obj.Subject {
return false
}
}
return true
}
func Send(em *Email) {
source := EmailTemplate(em.Template, em.Data)
m := gomail.NewMessage()
m.SetHeader("From", "info@betatude.com")
m.SetHeader("To", em.To)
m.SetHeader("Subject", em.Subject)
m.SetBody("text/html", source)
if err := emailQue.Mailer.DialAndSend(m); err != nil {
utils.Log(2, err)
}
emailQue.LastSent++
emailQue.LastSentTime = time.Now()
}
func SendFailureEmail(service *core.Service) {
email := &Email{
To: "info@socialeck.com",
Subject: fmt.Sprintf("Service %v is Failing", service.Name),
Template: "failure.html",
Data: service,
}
AddEmail(email)
}
func LoadMailer(config *core.Communication) *gomail.Dialer {
if config.Host == "" || config.Username == "" || config.Password == "" {
return nil
}
emailQue = new(Que)
emailQue.Outgoing = []*Email{}
emailQue.Mailer = gomail.NewDialer(config.Host, config.Port, config.Username, config.Password)
emailQue.Mailer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
return emailQue.Mailer
}
func EmailTemplate(tmpl string, data interface{}) string {
emailTpl, err := core.EmailBox.String(tmpl)
if err != nil {
utils.Log(3, err)
}
t := template.New("email")
t, err = t.Parse(emailTpl)
if err != nil {
utils.Log(3, err)
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, data); err != nil {
utils.Log(2, err)
}
result := tpl.String()
return result
}

View File

@ -7,4 +7,4 @@ import (
func Error404Handler(w http.ResponseWriter, r *http.Request) { func Error404Handler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
ExecuteResponse(w, r, "error_404.html", nil) ExecuteResponse(w, r, "error_404.html", nil)
} }

View File

@ -3,17 +3,17 @@ package handlers
import ( import (
"fmt" "fmt"
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/notifications"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"net/http" "net/http"
) )
func PluginsHandler(w http.ResponseWriter, r *http.Request) { func PluginsHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r) if !IsAuthenticated(r) {
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
//CoreApp.FetchPluginRepo() //CoreApp.FetchPluginRepo()
//var pluginFields []PluginSelect //var pluginFields []PluginSelect
@ -31,8 +31,7 @@ func PluginsHandler(w http.ResponseWriter, r *http.Request) {
} }
func SaveSettingsHandler(w http.ResponseWriter, r *http.Request) { func SaveSettingsHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r) if !IsAuthenticated(r) {
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@ -63,8 +62,7 @@ func SaveSettingsHandler(w http.ResponseWriter, r *http.Request) {
} }
func SaveSASSHandler(w http.ResponseWriter, r *http.Request) { func SaveSASSHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r) if !IsAuthenticated(r) {
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@ -78,8 +76,7 @@ func SaveSASSHandler(w http.ResponseWriter, r *http.Request) {
} }
func SaveAssetsHandler(w http.ResponseWriter, r *http.Request) { func SaveAssetsHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r) if !IsAuthenticated(r) {
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
@ -89,45 +86,64 @@ func SaveAssetsHandler(w http.ResponseWriter, r *http.Request) {
} }
func SaveEmailSettingsHandler(w http.ResponseWriter, r *http.Request) { func SaveEmailSettingsHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r) if !IsAuthenticated(r) {
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
emailer := core.SelectCommunication(1) emailer := core.SelectCommunication(1)
r.ParseForm() r.ParseForm()
emailer.Host = r.PostForm.Get("host") smtpHost := r.PostForm.Get("host")
emailer.Username = r.PostForm.Get("username") smtpUser := r.PostForm.Get("username")
emailer.Password = r.PostForm.Get("password") smtpPass := r.PostForm.Get("password")
emailer.Port = int(utils.StringInt(r.PostForm.Get("port"))) smtpPort := int(utils.StringInt(r.PostForm.Get("port")))
emailer.Var1 = r.PostForm.Get("address") smtpOutgoing := r.PostForm.Get("address")
enabled := r.PostForm.Get("enable_email")
emailer.Host = smtpHost
emailer.Username = smtpUser
if smtpPass != "#######################" {
emailer.Password = smtpPass
}
emailer.Port = smtpPort
emailer.Var1 = smtpOutgoing
emailer.Enabled = false
if enabled == "on" {
emailer.Enabled = true
}
core.Update(emailer) core.Update(emailer)
//sample := &Email{ sample := &types.Email{
// To: SessionUser(r).Email, To: SessionUser(r).Email,
// Subject: "Sample Email", Subject: "Test Email",
// Template: "error.html", Template: "message.html",
//} From: emailer.Var1,
//AddEmail(sample) }
notifications.LoadEmailer(emailer)
notifications.SendEmail(core.EmailBox, sample)
notifications.EmailComm = emailer
if emailer.Enabled {
utils.Log(1, "Starting Email Routine, 1 unique email per 60 seconds")
go notifications.EmailRoutine()
}
http.Redirect(w, r, "/settings", http.StatusSeeOther) http.Redirect(w, r, "/settings", http.StatusSeeOther)
} }
func SaveSlackSettingsHandler(w http.ResponseWriter, r *http.Request) { func SaveSlackSettingsHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r) if !IsAuthenticated(r) {
if !auth {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
slack := core.SelectCommunication(2) slack := core.SelectCommunication(2)
r.ParseForm() r.ParseForm()
slack.Host = r.PostForm.Get("host") slackWebhook := r.PostForm.Get("slack_url")
slack.Enabled = true enable := r.PostForm.Get("enable_slack")
if slack.Host == "" { slack.Enabled = false
slack.Enabled = false if enable == "on" && slackWebhook != "" {
slack.Enabled = true
go notifications.SlackRoutine()
} }
slack.Host = slackWebhook
core.Update(slack) core.Update(slack)
core.SendSlackMessage("This is a test from Statup!")
http.Redirect(w, r, "/settings", http.StatusSeeOther) http.Redirect(w, r, "/settings", http.StatusSeeOther)
} }

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"time"
) )
func SetupHandler(w http.ResponseWriter, r *http.Request) { func SetupHandler(w http.ResponseWriter, r *http.Request) {
@ -14,6 +15,7 @@ func SetupHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
w.WriteHeader(http.StatusOK)
port := 5432 port := 5432
if os.Getenv("DB_CONN") == "mysql" { if os.Getenv("DB_CONN") == "mysql" {
port = 3306 port = 3306
@ -109,15 +111,13 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
} }
admin.Create() admin.Create()
core.InsertDefaultComms()
if sample == "on" { if sample == "on" {
go core.LoadSampleData() core.LoadSampleData()
} }
core.SelectCore() core.InitApp()
time.Sleep(2 * time.Second)
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
//mainProcess()
} }
func SetupResponseError(w http.ResponseWriter, r *http.Request, a interface{}) { func SetupResponseError(w http.ResponseWriter, r *http.Request, a interface{}) {

View File

@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"fmt"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/types" "github.com/hunterlong/statup/types"
@ -39,15 +38,17 @@ func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
fmt.Println("creating user")
r.ParseForm() r.ParseForm()
username := r.PostForm.Get("username") username := r.PostForm.Get("username")
password := r.PostForm.Get("password") password := r.PostForm.Get("password")
email := r.PostForm.Get("email") email := r.PostForm.Get("email")
admin := r.PostForm.Get("admin")
user := &core.User{ user := &core.User{
Username: username, Username: username,
Password: password, Password: password,
Email: email, Email: email,
Admin: (admin == "on"),
} }
_, err := user.Create() _, err := user.Create()
if err != nil { if err != nil {

15
main.go
View File

@ -65,20 +65,7 @@ func mainProcess() {
utils.Log(3, err) utils.Log(3, err)
} }
core.RunDatabaseUpgrades() core.RunDatabaseUpgrades()
core.CoreApp, err = core.SelectCore() core.InitApp()
if err != nil {
utils.Log(2, "Core database was not found, Statup is not setup yet.")
handlers.RunHTTPServer()
}
core.CheckServices()
core.CoreApp.Communications, err = core.SelectAllCommunications()
if err != nil {
utils.Log(2, err)
}
core.LoadDefaultCommunications()
go core.DatabaseMaintence()
if !core.SetupMode { if !core.SetupMode {
LoadPlugins() LoadPlugins()

105
notifications/email.go Normal file
View File

@ -0,0 +1,105 @@
package notifications
import (
"bytes"
"crypto/tls"
"fmt"
"github.com/GeertJohan/go.rice"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"gopkg.in/gomail.v2"
"html/template"
"time"
)
var (
mailer *gomail.Dialer
emailQueue []*types.Email
EmailComm *types.Communication
)
func EmailRoutine() {
var sentAddresses []string
for _, email := range emailQueue {
if inArray(sentAddresses, email.To) || email.Sent {
emailQueue = removeEmail(emailQueue, email)
continue
}
e := email
go func(email *types.Email) {
err := dialSend(email)
if err == nil {
email.Sent = true
sentAddresses = append(sentAddresses, email.To)
utils.Log(1, fmt.Sprintf("Email '%v' sent to: %v using the %v template (size: %v)", email.Subject, email.To, email.Template, len([]byte(email.Source))))
emailQueue = removeEmail(emailQueue, email)
}
}(e)
}
time.Sleep(60 * time.Second)
if EmailComm.Enabled {
EmailRoutine()
}
}
func dialSend(email *types.Email) error {
m := gomail.NewMessage()
m.SetHeader("From", email.From)
m.SetHeader("To", email.To)
m.SetHeader("Subject", email.Subject)
m.SetBody("text/html", email.Source)
if err := mailer.DialAndSend(m); err != nil {
utils.Log(3, fmt.Sprintf("Email '%v' sent to: %v using the %v template (size: %v) %v", email.Subject, email.To, email.Template, len([]byte(email.Source)), err))
return err
}
return nil
}
func LoadEmailer(mail *types.Communication) {
utils.Log(1, fmt.Sprintf("Loading SMTP Emailer using host: %v:%v", mail.Host, mail.Port))
mailer = gomail.NewDialer(mail.Host, mail.Port, mail.Username, mail.Password)
mailer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
}
func SendEmail(box *rice.Box, email *types.Email) {
source := EmailTemplate(box, email.Template, email.Data)
email.Source = source
emailQueue = append(emailQueue, email)
}
func EmailTemplate(box *rice.Box, tmpl string, data interface{}) string {
emailTpl, err := box.String(tmpl)
if err != nil {
utils.Log(3, err)
}
t := template.New("email")
t, err = t.Parse(emailTpl)
if err != nil {
utils.Log(3, err)
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, data); err != nil {
utils.Log(2, err)
}
result := tpl.String()
return result
}
func removeEmail(emails []*types.Email, em *types.Email) []*types.Email {
var newArr []*types.Email
for _, e := range emails {
if e != em {
newArr = append(newArr, e)
}
}
return newArr
}
func inArray(a []string, v string) bool {
for _, i := range a {
if i == v {
return true
}
}
return false
}

61
notifications/slack.go Normal file
View File

@ -0,0 +1,61 @@
package notifications
import (
"bytes"
"fmt"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"github.com/pkg/errors"
"net/http"
"time"
)
var (
slackUrl string
sentLastMin int
slackMessages []string
SlackComm *types.Communication
)
func LoadSlack(url string) {
if url == "" {
utils.Log(1, "Slack Webhook URL is empty")
return
}
slackUrl = url
}
func SlackRoutine() {
for _, msg := range slackMessages {
utils.Log(1, fmt.Sprintf("Sending JSON to Slack Webhook: %v", msg))
client := http.Client{Timeout: 15 * time.Second}
_, err := client.Post(slackUrl, "application/json", bytes.NewBuffer([]byte(msg)))
if err != nil {
utils.Log(3, fmt.Sprintf("Issue sending Slack notification: %v", err))
}
slackMessages = removeStrArray(slackMessages, msg)
}
time.Sleep(60 * time.Second)
if SlackComm.Enabled {
SlackRoutine()
}
}
func removeStrArray(arr []string, v string) []string {
var newArray []string
for _, i := range arr {
if i != v {
newArray = append(newArray, v)
}
}
return newArray
}
func SendSlack(msg string) error {
if slackUrl == "" {
return errors.New("Slack Webhook URL has not been set in settings")
}
fullMessage := fmt.Sprintf("{\"text\":\"%v\"}", msg)
slackMessages = append(slackMessages, fullMessage)
return nil
}

View File

@ -189,6 +189,117 @@ HTML, BODY {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; }
.switch {
font-size: 1rem;
position: relative; }
.switch input {
position: absolute;
height: 1px;
width: 1px;
background: none;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
padding: 0; }
.switch input + label {
position: relative;
min-width: calc(calc(2.375rem * .8) * 2);
border-radius: calc(2.375rem * .8);
height: calc(2.375rem * .8);
line-height: calc(2.375rem * .8);
display: inline-block;
cursor: pointer;
outline: none;
user-select: none;
vertical-align: middle;
text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem); }
.switch input + label::before,
.switch input + label::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: calc(calc(2.375rem * .8) * 2);
bottom: 0;
display: block; }
.switch input + label::before {
right: 0;
background-color: #dee2e6;
border-radius: calc(2.375rem * .8);
transition: 0.2s all; }
.switch input + label::after {
top: 2px;
left: 2px;
width: calc(calc(2.375rem * .8) - calc(2px * 2));
height: calc(calc(2.375rem * .8) - calc(2px * 2));
border-radius: 50%;
background-color: white;
transition: 0.2s all; }
.switch input:checked + label::before {
background-color: #08d; }
.switch input:checked + label::after {
margin-left: calc(2.375rem * .8); }
.switch input:focus + label::before {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25); }
.switch input:disabled + label {
color: #868e96;
cursor: not-allowed; }
.switch input:disabled + label::before {
background-color: #e9ecef; }
.switch.switch-sm {
font-size: 0.875rem; }
.switch.switch-sm input + label {
min-width: calc(calc(1.9375rem * .8) * 2);
height: calc(1.9375rem * .8);
line-height: calc(1.9375rem * .8);
text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem); }
.switch.switch-sm input + label::before {
width: calc(calc(1.9375rem * .8) * 2); }
.switch.switch-sm input + label::after {
width: calc(calc(1.9375rem * .8) - calc(2px * 2));
height: calc(calc(1.9375rem * .8) - calc(2px * 2)); }
.switch.switch-sm input:checked + label::after {
margin-left: calc(1.9375rem * .8); }
.switch.switch-lg {
font-size: 1.25rem; }
.switch.switch-lg input + label {
min-width: calc(calc(3rem * .8) * 2);
height: calc(3rem * .8);
line-height: calc(3rem * .8);
text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem); }
.switch.switch-lg input + label::before {
width: calc(calc(3rem * .8) * 2); }
.switch.switch-lg input + label::after {
width: calc(calc(3rem * .8) - calc(2px * 2));
height: calc(calc(3rem * .8) - calc(2px * 2)); }
.switch.switch-lg input:checked + label::after {
margin-left: calc(3rem * .8); }
.switch + .switch {
margin-left: 1rem; }
@keyframes pulse_animation { @keyframes pulse_animation {
0% { 0% {
transform: scale(1); } transform: scale(1); }

File diff suppressed because one or more lines are too long

View File

@ -36,19 +36,24 @@
<tr> <tr>
<td class="content-cell" style="box-sizing: border-box; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; padding: 35px; word-break: break-word;"> <td class="content-cell" style="box-sizing: border-box; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; padding: 35px; word-break: break-word;">
<h1 style="box-sizing: border-box; color: #2F3133; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 19px; font-weight: bold; margin-top: 0;" align="left">{{ .Name }} is Offline!</h1> <h1 style="box-sizing: border-box; color: #2F3133; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 19px; font-weight: bold; margin-top: 0;" align="left">{{ .Service.Name }} is Offline!</h1>
<p style="box-sizing: border-box; color: #74787E; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 16px; line-height: 1.5em; margin-top: 0;" align="left"> <p style="box-sizing: border-box; color: #74787E; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 16px; line-height: 1.5em; margin-top: 0;" align="left">
Your Statup service named '{{.Name}}' has been triggered with a HTTP status code of '{{.LastStatusCode}}' and is currently offline based on your requirements. Your Statup service '<a target="_blank" href="{{.Service.Domain}}">{{.Service.Name}}</a>' has been triggered with a HTTP status code of '{{.Service.LastStatusCode}}' and is currently offline based on your requirements. This failure was created on {{.Service.CreatedAt}}.
</p> </p>
{{if .Service.LastResponse }}
<h1 style="box-sizing: border-box; color: #2F3133; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 19px; font-weight: bold; margin-top: 0;" align="left">Last Response</h1> <h1 style="box-sizing: border-box; color: #2F3133; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 19px; font-weight: bold; margin-top: 0;" align="left">Last Response</h1>
<p style="box-sizing: border-box; color: #74787E; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 16px; line-height: 1.5em; margin-top: 0;" align="left"> <p style="box-sizing: border-box; color: #74787E; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 16px; line-height: 1.5em; margin-top: 0;" align="left">
{{ .LastResponse }} {{ .Service.LastResponse }}
</p> </p>
{{end}}
<table class="body-sub" style="border-top-color: #EDEFF2; border-top-style: solid; border-top-width: 1px; box-sizing: border-box; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin-top: 25px; padding-top: 25px;"> <table class="body-sub" style="border-top-color: #EDEFF2; border-top-style: solid; border-top-width: 1px; box-sizing: border-box; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin-top: 25px; padding-top: 25px;">
<td style="box-sizing: border-box; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; word-break: break-word;"> <td style="box-sizing: border-box; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; word-break: break-word;">
<a href="/service/{{.Id}}" class="button button--blue" target="_blank" style="-webkit-text-size-adjust: none; background: #3869D4; border-color: #3869d4; border-radius: 3px; border-style: solid; border-width: 10px 18px; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); box-sizing: border-box; color: #FFF; display: inline-block; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; text-decoration: none;">View Service</a> <a href="{{.Domain}}/service/{{.Service.Id}}" class="button button--blue" target="_blank" style="-webkit-text-size-adjust: none; background: #3869D4; border-color: #3869d4; border-radius: 3px; border-style: solid; border-width: 10px 18px; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); box-sizing: border-box; color: #FFF; display: inline-block; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; text-decoration: none;">View Service</a>
</td>
<td style="box-sizing: border-box; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; word-break: break-word;">
<a href="{{.Domain}}/dashboard" class="button button--blue" target="_blank" style="-webkit-text-size-adjust: none; background: #3869D4; border-color: #3869d4; border-radius: 3px; border-style: solid; border-width: 10px 18px; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); box-sizing: border-box; color: #FFF; display: inline-block; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; text-decoration: none;">Statup Dashboard</a>
</td> </td>
</table> </table>
</td> </td>

View File

@ -36,7 +36,7 @@
<tr> <tr>
<td class="content-cell" style="box-sizing: border-box; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; padding: 35px; word-break: break-word;"> <td class="content-cell" style="box-sizing: border-box; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; padding: 35px; word-break: break-word;">
<h1 style="box-sizing: border-box; color: #2F3133; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 19px; font-weight: bold; margin-top: 0;" align="left">Looks like emails work!</h1> <h1 style="box-sizing: border-box; color: #2F3133; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 19px; font-weight: bold; margin-top: 0;" align="left">Looks Like Emails Work!</h1>
<p style="box-sizing: border-box; color: #74787E; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 16px; line-height: 1.5em; margin-top: 0;" align="left"> <p style="box-sizing: border-box; color: #74787E; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 16px; line-height: 1.5em; margin-top: 0;" align="left">
Since you got this email, it confirms that your Statup Status Page email system is working correctly. Since you got this email, it confirms that your Statup Status Page email system is working correctly.
</p> </p>

View File

@ -242,6 +242,119 @@ HTML,BODY {
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
} }
.switch {
font-size: 1rem;
position: relative;
}
.switch input {
position: absolute;
height: 1px;
width: 1px;
background: none;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
padding: 0;
}
.switch input + label {
position: relative;
min-width: calc(calc(2.375rem * .8) * 2);
border-radius: calc(2.375rem * .8);
height: calc(2.375rem * .8);
line-height: calc(2.375rem * .8);
display: inline-block;
cursor: pointer;
outline: none;
user-select: none;
vertical-align: middle;
text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem);
}
.switch input + label::before,
.switch input + label::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: calc(calc(2.375rem * .8) * 2);
bottom: 0;
display: block;
}
.switch input + label::before {
right: 0;
background-color: #dee2e6;
border-radius: calc(2.375rem * .8);
transition: 0.2s all;
}
.switch input + label::after {
top: 2px;
left: 2px;
width: calc(calc(2.375rem * .8) - calc(2px * 2));
height: calc(calc(2.375rem * .8) - calc(2px * 2));
border-radius: 50%;
background-color: white;
transition: 0.2s all;
}
.switch input:checked + label::before {
background-color: #08d;
}
.switch input:checked + label::after {
margin-left: calc(2.375rem * .8);
}
.switch input:focus + label::before {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25);
}
.switch input:disabled + label {
color: #868e96;
cursor: not-allowed;
}
.switch input:disabled + label::before {
background-color: #e9ecef;
}
.switch.switch-sm {
font-size: 0.875rem;
}
.switch.switch-sm input + label {
min-width: calc(calc(1.9375rem * .8) * 2);
height: calc(1.9375rem * .8);
line-height: calc(1.9375rem * .8);
text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem);
}
.switch.switch-sm input + label::before {
width: calc(calc(1.9375rem * .8) * 2);
}
.switch.switch-sm input + label::after {
width: calc(calc(1.9375rem * .8) - calc(2px * 2));
height: calc(calc(1.9375rem * .8) - calc(2px * 2));
}
.switch.switch-sm input:checked + label::after {
margin-left: calc(1.9375rem * .8);
}
.switch.switch-lg {
font-size: 1.25rem;
}
.switch.switch-lg input + label {
min-width: calc(calc(3rem * .8) * 2);
height: calc(3rem * .8);
line-height: calc(3rem * .8);
text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem);
}
.switch.switch-lg input + label::before {
width: calc(calc(3rem * .8) * 2);
}
.switch.switch-lg input + label::after {
width: calc(calc(3rem * .8) - calc(2px * 2));
height: calc(calc(3rem * .8) - calc(2px * 2));
}
.switch.switch-lg input:checked + label::after {
margin-left: calc(3rem * .8);
}
.switch + .switch {
margin-left: 1rem;
}
@keyframes pulse_animation { @keyframes pulse_animation {
0% { transform: scale(1); } 0% { transform: scale(1); }
30% { transform: scale(1); } 30% { transform: scale(1); }

View File

@ -79,22 +79,22 @@
<div class="form-group row"> <div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Service Name</label> <label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="name" class="form-control" id="service_name" value="{{.Name}}" placeholder="Name"> <input type="text" name="name" class="form-control" id="service_name" value="{{.Name}}" placeholder="Name" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Service Check Type</label> <label for="service_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select name="check_type" class="form-control" id="service_type" value="{{.Type}}"> <select name="check_type" class="form-control" id="service_type" value="{{.Type}}">
<option value="http" selected>HTTP Service</option> <option value="http" {{if eq .Type "http"}}selected{{end}}>HTTP Service</option>
<option value="tcp">TCP Service</option> <option value="tcp" {{if eq .Type "tcp"}}selected{{end}}>TCP Service</option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label> <label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="domain" class="form-control" id="service_url" value="{{.Domain}}" placeholder="https://google.com"> <input type="text" name="domain" class="form-control" id="service_url" value="{{.Domain}}" placeholder="https://google.com" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -121,7 +121,7 @@
<div class="form-group row"> <div class="form-group row">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label> <label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" value="{{.ExpectedStatus}}" id="service_response_code" value="200"> <input type="number" name="expected_status" class="form-control" value="{{.ExpectedStatus}}" id="service_response_code">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -133,7 +133,7 @@
<div class="form-group row"> <div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label> <label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="interval" class="form-control" value="{{.Interval}}" id="service_interval" placeholder="10"> <input type="number" name="interval" class="form-control" value="{{.Interval}}" id="service_interval" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">

View File

@ -50,7 +50,7 @@
<div class="form-group row"> <div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Service Name</label> <label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="name" class="form-control" id="service_name" placeholder="Name"> <input type="text" name="name" class="form-control" id="service_name" placeholder="Name" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -65,7 +65,7 @@
<div class="form-group row"> <div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label> <label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="domain" class="form-control" id="service_url" placeholder="https://google.com"> <input type="text" name="domain" class="form-control" id="service_url" placeholder="https://google.com" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -104,7 +104,7 @@
<div class="form-group row"> <div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label> <label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="interval" class="form-control" id="service_interval" placeholder="10"> <input type="number" name="interval" class="form-control" id="service_interval" value="60" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">

View File

@ -105,7 +105,7 @@
</div> </div>
<button type="submit" class="btn btn-primary btn-block mt-2">Save Style</button> <button type="submit" class="btn btn-primary btn-block mt-2">Save Style</button>
</form> </form>
{{end}} {{end}}
</div> </div>
{{ with $c := index .Communications 0 }} {{ with $c := index .Communications 0 }}
@ -125,7 +125,7 @@
<div class="form-group"> <div class="form-group">
<label for="password">SMTP Password</label> <label for="password">SMTP Password</label>
<input type="password" name="password" class="form-control" value="{{ $c.Password }}" id="password"> <input type="password" name="password" class="form-control" value="{{ if $c.Password }}#######################{{end}}" id="password">
</div> </div>
<div class="form-group"> <div class="form-group">
@ -143,7 +143,19 @@
<input type="number" name="limit" class="form-control" value="30" id="limit" placeholder="noreply@domain.com"> <input type="number" name="limit" class="form-control" value="30" id="limit" placeholder="noreply@domain.com">
</div> </div>
<button type="submit" class="btn btn-primary btn-block">Save Email Settings</button> <div class="form-group row">
<div class="col-sm-6">
<span class="switch">
<input type="checkbox" name="enable_{{ $c.Method }}" class="switch" id="switch-{{ $c.Method }}" {{if .Enabled}}checked{{end}}>
<label for="switch-{{ $c.Method }}">Enable Emails</label>
</span>
</div>
<div class="col-sm-6">
<button type="submit" class="btn btn-primary btn-block">Save Email Settings</button>
</div>
</div>
</form> </form>
@ -151,22 +163,34 @@
{{ end }} {{ end }}
{{ with $c := index .Communications 1 }} {{ with $c := index .Communications 1 }}
<div class="tab-pane fade" id="v-pills-{{ $c.Method }}" role="tabpanel" aria-labelledby="v-pills-{{ $c.Method }}-tab"> <div class="tab-pane fade" id="v-pills-{{ $c.Method }}" role="tabpanel" aria-labelledby="v-pills-{{ $c.Method }}-tab">
<form method="POST" action="/settings/{{ $c.Method }}"> <form method="POST" action="/settings/{{ $c.Method }}">
<div class="form-group"> <div class="form-group">
<label for="host">Slack Webhook URL</label> <label for="slack_url">Slack Webhook URL</label>
<input type="text" name="host" class="form-control" value="{{ $c.Host }}" id="host" placeholder="https://hooks.slack.com/services/TJIIDSJIFJ/729FJSDF/hua463asda9af79"> <input type="text" name="slack_url" class="form-control" value="{{ $c.Host }}" id="slack_url" placeholder="https://hooks.slack.com/services/TJIIDSJIFJ/729FJSDF/hua463asda9af79">
</div> </div>
<button type="submit" class="btn btn-primary btn-block">Save Slack Settings</button> <div class="form-group row">
<div class="col-sm-6">
<span class="switch">
<input type="checkbox" name="enable_{{ $c.Method }}" class="switch" id="switch-{{ $c.Method }}" {{if .Enabled}}checked{{end}}>
<label for="switch-{{ $c.Method }}">Enable Slack</label>
</span>
</div>
</form> <div class="col-sm-6">
<button type="submit" class="btn btn-primary btn-block">Save Slack Settings</button>
</div>
</div>
</div>
{{ end }} </form>
</div>
{{ end }}
<div class="tab-pane fade" id="v-pills-browse" role="tabpanel" aria-labelledby="v-pills-browse-tab"> <div class="tab-pane fade" id="v-pills-browse" role="tabpanel" aria-labelledby="v-pills-browse-tab">
{{ range .Repos }} {{ range .Repos }}

View File

@ -44,31 +44,37 @@
<form action="/users" method="POST"> <form action="/users" method="POST">
<div class="form-group row"> <div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Username</label> <label for="username" class="col-sm-4 col-form-label">Username</label>
<div class="col-sm-8"> <div class="col-sm-4">
<input type="text" name="username" class="form-control" id="username" placeholder="Username"> <input type="text" name="username" class="form-control" id="username" placeholder="Username" required>
</div>
<div class="col-sm-4">
<span class="switch">
<input type="checkbox" name="admin" class="switch" id="switch-normal">
<label for="switch-normal">Administrator</label>
</span>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="email" class="col-sm-4 col-form-label">Email Address</label> <label for="email" class="col-sm-4 col-form-label">Email Address</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="email" class="form-control" id="email" placeholder="user@domain.com"> <input type="email" name="email" class="form-control" id="email" placeholder="user@domain.com" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="password" class="col-sm-4 col-form-label">Password</label> <label for="password" class="col-sm-4 col-form-label">Password</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="password" name="password" class="form-control" id="password" placeholder="Password"> <input type="password" name="password" class="form-control" id="password" placeholder="Password" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="password_confirm" class="col-sm-4 col-form-label">Confirm Password</label> <label for="password_confirm" class="col-sm-4 col-form-label">Confirm Password</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="password" name="password_confirm" class="form-control" id="password_confirm" placeholder="Confirm Password"> <input type="password" name="password_confirm" class="form-control" id="password_confirm" placeholder="Confirm Password" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-8"> <div class="col-sm-12">
<button type="submit" class="btn btn-primary">Create User</button> <button type="submit" class="btn btn-primary btn-block">Create User</button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -61,7 +61,10 @@ type Email struct {
To string To string
Subject string Subject string
Template string Template string
From string
Data interface{} Data interface{}
Source string
Sent bool
} }
type Config struct { type Config struct {