Initial Commit
This commit is contained in:
parent
32e120ddaa
commit
c6af685621
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/simpledash
|
||||||
|
/simpledash.toml
|
||||||
|
/sessions.db
|
||||||
|
.idea/
|
33
config.go
Normal file
33
config.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Conf struct stores root of TOML config
|
||||||
|
type Conf struct {
|
||||||
|
Title string
|
||||||
|
Session SessionConf
|
||||||
|
AllowProxy []string
|
||||||
|
LoginRequired bool
|
||||||
|
Theme string
|
||||||
|
Users map[string]User
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionConf stores session configuration
|
||||||
|
type SessionConf struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// User stores user configuration from TOML
|
||||||
|
type User struct {
|
||||||
|
PasswordHash string
|
||||||
|
ShowPublic bool
|
||||||
|
Cards []Card `toml:"card"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card stores card configuration from TOML
|
||||||
|
type Card struct {
|
||||||
|
Type string `toml:"type"`
|
||||||
|
Title string `toml:"title"`
|
||||||
|
Description string `toml:"desc,omitempty"`
|
||||||
|
Icon string `toml:"icon,omitempty"`
|
||||||
|
URL string `toml:"url,omitempty"`
|
||||||
|
Data map[string]interface{} `toml:"data,omitempty"`
|
||||||
|
}
|
43
extra.go
Normal file
43
extra.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send error to HTTP response
|
||||||
|
func httpError(res http.ResponseWriter, errTmpl *template.Template, config Conf, statusCode int, reason string) {
|
||||||
|
// Write error code to response
|
||||||
|
res.WriteHeader(statusCode)
|
||||||
|
// Execute error template, outputting to response
|
||||||
|
err := errTmpl.Execute(res, map[string]interface{}{
|
||||||
|
"StatusCode": statusCode,
|
||||||
|
"Reason": reason,
|
||||||
|
"Config": config,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Error occurred while handling error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressRes(res http.ResponseWriter) *gzip.Writer {
|
||||||
|
// Set response header to reflect gzip compression
|
||||||
|
res.Header().Set("Content-Encoding", "gzip")
|
||||||
|
// Wrap response in gzip writer
|
||||||
|
return gzip.NewWriter(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a string slice contains a string
|
||||||
|
func strSlcContains(slice []string, str string) bool {
|
||||||
|
// For every value in slice
|
||||||
|
for _, val := range slice {
|
||||||
|
// If value is contained in provided string, return true
|
||||||
|
if strings.Contains(str, val) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
22
go.mod
Normal file
22
go.mod
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
module simpledash
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
|
github.com/Masterminds/semver v1.5.0 // indirect
|
||||||
|
github.com/Masterminds/sprig v2.22.0+incompatible
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/google/uuid v1.2.0 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.0
|
||||||
|
github.com/huandu/xstrings v1.3.2 // indirect
|
||||||
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
|
github.com/mitchellh/copystructure v1.1.1 // indirect
|
||||||
|
github.com/pelletier/go-toml v1.8.1
|
||||||
|
github.com/rs/zerolog v1.21.0
|
||||||
|
github.com/spf13/pflag v1.0.5
|
||||||
|
github.com/wader/gormstore/v2 v2.0.0
|
||||||
|
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
|
||||||
|
gorm.io/driver/sqlite v1.1.4
|
||||||
|
gorm.io/gorm v1.21.5
|
||||||
|
)
|
238
go.sum
Normal file
238
go.sum
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
|
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
|
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
|
||||||
|
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||||
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
||||||
|
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||||
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
|
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
||||||
|
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||||
|
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
|
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||||
|
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||||
|
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
|
||||||
|
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||||
|
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||||
|
github.com/jackc/pgconn v1.8.0 h1:FmjZ0rOyXTr1wfWs45i4a9vjnjWUAGpMuQLD9OSs+lw=
|
||||||
|
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||||
|
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||||
|
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
||||||
|
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.7 h1:6Pwi1b3QdY65cuv6SyVO0FgPd5J3Bl7wf/nQQjinHMA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||||
|
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||||
|
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
|
||||||
|
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
||||||
|
github.com/jackc/pgtype v1.6.2 h1:b3pDeuhbbzBYcg5kwNmNDun4pFUD/0AAr1kLXZLeNt8=
|
||||||
|
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||||
|
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
|
||||||
|
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
|
||||||
|
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
||||||
|
github.com/jackc/pgx/v4 v4.10.1 h1:/6Q3ye4myIj6AaplUm+eRcz4OhK9HAvFf4ePsG40LJY=
|
||||||
|
github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||||
|
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||||
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||||
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4=
|
||||||
|
github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
|
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
|
||||||
|
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
|
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||||
|
github.com/rs/zerolog v1.21.0 h1:Q3vdXlfLNT+OftyBHsU0Y445MD+8m8axjKgf2si0QcM=
|
||||||
|
github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
|
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/wader/gormstore/v2 v2.0.0 h1:Idfd68RXNFibVmkNKgNv8l7BobUfyvwEm1gvWqeA/Yw=
|
||||||
|
github.com/wader/gormstore/v2 v2.0.0/go.mod h1:3BgNKFxRdVo2E4pq3e/eiim8qRDZzaveaIcIvu2T8r0=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||||
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
|
||||||
|
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||||
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.0.4 h1:TATTzt+kR+IV0+h3iUB3dHUe8omCvQ0rOkmfCsUBohk=
|
||||||
|
gorm.io/driver/mysql v1.0.4/go.mod h1:MEgp8tk2n60cSBCq5iTcPDw3ns8Gs+zOva9EUhkknTs=
|
||||||
|
gorm.io/driver/postgres v1.0.8 h1:PAgM+PaHOSAeroTjHkCHCBIHHoBIf9RgPWGo8dF2DA8=
|
||||||
|
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
|
||||||
|
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
||||||
|
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
||||||
|
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
|
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
|
gorm.io/gorm v1.21.5 h1:Qf3uCq1WR9lt9/udefdhaFcf+aAZ+mrDtfXTA+GB9Gc=
|
||||||
|
gorm.io/gorm v1.21.5/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
125
main.go
Normal file
125
main.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/Masterminds/sprig"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/pelletier/go-toml"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
"github.com/wader/gormstore/v2"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
"html/template"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set global logger to ConsoleWriter
|
||||||
|
var Log = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
Route *mux.Router
|
||||||
|
Templates map[string]*template.Template
|
||||||
|
Session *gormstore.Store
|
||||||
|
Config Conf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new empty map to store templates
|
||||||
|
var templates = map[string]*template.Template{}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create command-line flags
|
||||||
|
addr := flag.IPP("addr", "a", net.ParseIP("0.0.0.0"), "Bind address for HTTP server")
|
||||||
|
port := flag.IntP("port", "p", 8080, "Bind port for HTTP server")
|
||||||
|
config := flag.StringP("config", "c", "simpledash.toml", "TOML config file")
|
||||||
|
// Parse flags
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Create new router
|
||||||
|
router := mux.NewRouter().StrictSlash(true)
|
||||||
|
|
||||||
|
// Create OS-specific glob for all templates
|
||||||
|
path := filepath.Join("resources", "templates", "*.html")
|
||||||
|
// Create OS-specific glob for all card templates
|
||||||
|
cardGlob := filepath.Join("resources", "templates", "cards", "*.html")
|
||||||
|
// Get all template paths
|
||||||
|
tmplMatches, _ := filepath.Glob(path)
|
||||||
|
cardMatches, _ := filepath.Glob(cardGlob)
|
||||||
|
matches := append(tmplMatches, cardMatches...)
|
||||||
|
// For each template path
|
||||||
|
for _, match := range matches {
|
||||||
|
// Get name of file without path or extension
|
||||||
|
fileName := strings.TrimSuffix(filepath.Base(match), filepath.Ext(match))
|
||||||
|
// If file is called base
|
||||||
|
if fileName == "base" {
|
||||||
|
// Skip
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
// Parse detected template and base template, add to templates map
|
||||||
|
templates[fileName], err = template.New(
|
||||||
|
filepath.Base(match)).Funcs(
|
||||||
|
sprig.FuncMap()).Funcs(
|
||||||
|
getFuncMap()).ParseFiles(
|
||||||
|
"resources/templates/base.html", match)
|
||||||
|
if err != nil {
|
||||||
|
Log.Fatal().Str("template", fileName).Err(err).Msg("Error parsing template")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open sqlite database called sessions.db for storing sessions
|
||||||
|
sessionDB, _ := gorm.Open(sqlite.Open("sessions.db"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||||
|
// Create session store from database
|
||||||
|
sessionStore := gormstore.New(sessionDB, []byte(""))
|
||||||
|
|
||||||
|
// Create channel to stop periodic cleanup
|
||||||
|
quitCleanup := make(chan struct{})
|
||||||
|
// Clean up expired sessions every hour
|
||||||
|
go sessionStore.PeriodicCleanup(1*time.Hour, quitCleanup)
|
||||||
|
|
||||||
|
// Open config file
|
||||||
|
configFile, err := os.Open(filepath.Clean(*config))
|
||||||
|
if err != nil {
|
||||||
|
Log.Fatal().Err(err).Msg("Error opening config file")
|
||||||
|
}
|
||||||
|
// Create new TOML decoder
|
||||||
|
dec := toml.NewDecoder(configFile)
|
||||||
|
// Create new nil variable to store decoded config
|
||||||
|
var decodedConf Conf
|
||||||
|
// Decode config into variable
|
||||||
|
err = dec.Decode(&decodedConf)
|
||||||
|
if err != nil {
|
||||||
|
Log.Fatal().Err(err).Msg("Error decoding config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register HTTP routes
|
||||||
|
registerRoutes(App{
|
||||||
|
Route: router,
|
||||||
|
Templates: templates,
|
||||||
|
Session: sessionStore,
|
||||||
|
Config: decodedConf,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create string address from flag values
|
||||||
|
strAddr := fmt.Sprint(*addr, ":", *port)
|
||||||
|
// Create listener on IPv4 using address created above
|
||||||
|
ln, err := net.Listen("tcp4", strAddr)
|
||||||
|
if err != nil {
|
||||||
|
Log.Fatal().Err(err).Msg("Error creating listener")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log HTTP server start
|
||||||
|
Log.Info().Str("addr", strAddr).Msg("Starting HTTP server")
|
||||||
|
// Start HTTP server using previously-created router and listener
|
||||||
|
err = http.Serve(ln, router)
|
||||||
|
if err != nil {
|
||||||
|
Log.Fatal().Err(err).Msg("Error while serving")
|
||||||
|
}
|
||||||
|
}
|
1
resources/public/css/bulma.min.css
vendored
Normal file
1
resources/public/css/bulma.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4742
resources/public/css/darkreader.css
Normal file
4742
resources/public/css/darkreader.css
Normal file
File diff suppressed because it is too large
Load Diff
13
resources/public/js/iconify.min.js
vendored
Normal file
13
resources/public/js/iconify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
65
resources/templates/base.html
Normal file
65
resources/templates/base.html
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{{define "head"}}
|
||||||
|
<title>{{.SiteTitle}} - {{.PageTitle}}</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/bulma.min.css">
|
||||||
|
<style>
|
||||||
|
::-webkit-scrollbar {border-radius: 24px; width: 8px;}
|
||||||
|
::-webkit-scrollbar-thumb {background: #e5e5e5; border-radius: 10px;}
|
||||||
|
</style>
|
||||||
|
<script async src="/js/iconify.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function toggleNavMenu() {
|
||||||
|
const navMenu = document.getElementById("navMenu");
|
||||||
|
const navbarBurger = document.getElementById("navbarBurger");
|
||||||
|
if (navMenu.classList.contains("is-active")) {
|
||||||
|
navMenu.classList.remove("is-active")
|
||||||
|
navbarBurger.classList.remove("is-active")
|
||||||
|
} else {
|
||||||
|
navMenu.classList.add("is-active")
|
||||||
|
navbarBurger.classList.add("is-active")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{if eq .Theme "dark"}}
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/darkreader.css">
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "navbar"}}
|
||||||
|
<nav class="navbar" role="navigation" aria-label="main nav" >
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item" href="/">{{.SiteTitle}}</a>
|
||||||
|
|
||||||
|
<a role="button" class="navbar-burger" onclick="toggleNavMenu()" aria-label="menu" aria-expanded="false" id="navbarBurger">
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-menu" id="navMenu">
|
||||||
|
<div class="navbar-end">
|
||||||
|
<a class="navbar-item {{if eq (print .Page) `home`}}is-active{{end}}" href="/">Home</a>
|
||||||
|
{{if and .User (ne .User "_public_")}}
|
||||||
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
|
<a class="navbar-link">{{.User}}</a>
|
||||||
|
<div class="navbar-dropdown">
|
||||||
|
<a class="navbar-item" href="/logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else if eq .User "_public_"}}
|
||||||
|
<a class="navbar-item {{if eq (print .Page) `login`}}is-active{{end}}" href="/login">Login</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "icon"}}
|
||||||
|
<span class="iconify icon:{{.}} icon-inline:false"></span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "icon-inline"}}
|
||||||
|
<span class="iconify icon:{{.}}"></span>
|
||||||
|
{{end}}
|
21
resources/templates/cards/collection.html
Normal file
21
resources/templates/cards/collection.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<div class="card-header">
|
||||||
|
<p class="card-header-title">{{.Title}}</p>
|
||||||
|
{{if ne .Icon ""}}
|
||||||
|
<div class="card-header-icon subtitle">
|
||||||
|
{{template "icon" .Icon}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
{{ range $name, $info := .Data }}
|
||||||
|
{{$data := dict "target" ""}}
|
||||||
|
{{- if eq $info.target "sameTab" -}}
|
||||||
|
{{- $_ := set $data "target" "_self" -}}
|
||||||
|
{{- else if eq $info.target "newTab" -}}
|
||||||
|
{{- $_ := set $data "target" "_blank" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- $_ := set $data "target" "_self" -}}
|
||||||
|
{{- end -}}
|
||||||
|
<a href="{{$info.url}}" class="button is-fullwidth has-text-left is-justify-content-start" target="{{$data.target}}" style="margin-bottom: 0.5rem; background-color: #f5f5f5">{{$name}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
18
resources/templates/cards/simple.html
Normal file
18
resources/templates/cards/simple.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div class="card-header">
|
||||||
|
<a class="card-header-title" href="{{.URL}}">
|
||||||
|
{{if ne .Icon ""}}
|
||||||
|
{{template "icon" .Icon}}
|
||||||
|
{{end}}
|
||||||
|
{{.Title}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{if ne .Description ""}}
|
||||||
|
<div class="card-content">
|
||||||
|
<p>{{.Description}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if ne .URL ""}}
|
||||||
|
<div class="card-footer" style="margin-top: auto">
|
||||||
|
<a class="card-footer-item has-text-info" href="{{.URL}}">{{.URL}}</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
39
resources/templates/cards/status.html
Normal file
39
resources/templates/cards/status.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<div class="card-header">
|
||||||
|
<a class="card-header-title" href="{{.URL}}">
|
||||||
|
{{if ne .Icon ""}}
|
||||||
|
{{template "icon" .Icon}}
|
||||||
|
{{end}}
|
||||||
|
{{.Title}}
|
||||||
|
</a>
|
||||||
|
<div class="card-header-icon">
|
||||||
|
<div class="tags has-addons">
|
||||||
|
<p class="tag">Status</p>
|
||||||
|
<p class="tag is-warning" id="{{.Title}}Status">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if ne .Description ""}}
|
||||||
|
<div class="card-content">
|
||||||
|
<p>{{.Description}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="card-footer" style="margin-top: auto">
|
||||||
|
<a class="card-footer-item has-text-info" href="{{.URL}}">{{.URL}}</a>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var request = new XMLHttpRequest()
|
||||||
|
request.open('GET', "/status/{{b64enc .URL}}", true)
|
||||||
|
request.onload = function () {
|
||||||
|
var data = JSON.parse(this.response)
|
||||||
|
if (data.down === true || parseInt(data.code) > 500 && parseInt(data.code) < 600 ) {
|
||||||
|
document.getElementById('{{.Title}}Status').classList.remove("is-warning")
|
||||||
|
document.getElementById('{{.Title}}Status').classList.add("is-danger")
|
||||||
|
document.getElementById('{{.Title}}Status').innerHTML = "Offline"
|
||||||
|
} else {
|
||||||
|
document.getElementById('{{.Title}}Status').classList.remove("is-warning")
|
||||||
|
document.getElementById('{{.Title}}Status').classList.add("is-success")
|
||||||
|
document.getElementById('{{.Title}}Status').innerHTML = "Online"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.send()
|
||||||
|
</script>
|
43
resources/templates/cards/weather.html
Normal file
43
resources/templates/cards/weather.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<div class="card-header">
|
||||||
|
<p class="card-header-title">{{.Title}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<p id="weatherLoadingText">Loading...</p>
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column is-half">
|
||||||
|
<object type="image/svg+xml" id="weatherStateImg" style="width:45px; height: 45px"></object>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half">
|
||||||
|
<p id="weatherTempText" class="has-text-right subtitle"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="subtitle is-marginless" id="weatherStateText"></p>
|
||||||
|
<p id="weatherMinText"></p>
|
||||||
|
<p id="weatherMaxText"></p>
|
||||||
|
<p id="weatherWindSpeedText"></p>
|
||||||
|
<p id="weatherHumidityText"></p>
|
||||||
|
<p id="weatherVisibilityText"></p>
|
||||||
|
<p id="weatherPredictabilityText"></p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer" style="margin-top: auto">
|
||||||
|
<span class="card-footer-item">Data from <a href="https://www.metaweather.com" class="has-text-info">Metaweather</a></span>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var request = new XMLHttpRequest()
|
||||||
|
request.open('GET', "{{proxy (printf `https://www.metaweather.com/api/location/%s/` .Data.woeid) }}", true)
|
||||||
|
const round = function (flt){return Number.parseFloat(flt).toPrecision(3)}
|
||||||
|
request.onload = function () {
|
||||||
|
const data = JSON.parse(this.response)
|
||||||
|
document.getElementById('weatherLoadingText').classList.add("is-hidden")
|
||||||
|
document.getElementById('weatherStateText').innerText = data["consolidated_weather"][0]["weather_state_name"]
|
||||||
|
document.getElementById('weatherTempText').innerHTML = round(data["consolidated_weather"][0]["the_temp"]*1.8+32) + " °F"
|
||||||
|
document.getElementById('weatherStateImg').data = "/proxy/" + btoa("https://www.metaweather.com/static/img/weather/" + data["consolidated_weather"][0]["weather_state_abbr"] + ".svg")
|
||||||
|
document.getElementById('weatherMinText').innerHTML = "Min: " + round(data["consolidated_weather"][0]["min_temp"]*1.8+32) + " °F"
|
||||||
|
document.getElementById('weatherMaxText').innerHTML = "Max: " + round(data["consolidated_weather"][0]["max_temp"]*1.8+32) + " °F"
|
||||||
|
document.getElementById('weatherWindSpeedText').innerText = "Wind Speed: " + round(data["consolidated_weather"][0]["wind_speed"]) + "mph"
|
||||||
|
document.getElementById('weatherHumidityText').innerText = "Humidity: " + data["consolidated_weather"][0]["humidity"] + "%"
|
||||||
|
document.getElementById('weatherVisibilityText').innerText = "Visibility: " + round(data["consolidated_weather"][0]["visibility"]) + "mi"
|
||||||
|
document.getElementById('weatherPredictabilityText').innerText = "Predictability: " + data["consolidated_weather"][0]["predictability"] + "%"
|
||||||
|
}
|
||||||
|
request.send()
|
||||||
|
</script>
|
24
resources/templates/error.html
Normal file
24
resources/templates/error.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{template "head" dict
|
||||||
|
"SiteTitle" .Config.Title
|
||||||
|
"PageTitle" "Error"
|
||||||
|
"Theme" .Config.Theme}}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" dict
|
||||||
|
"SiteTitle" .Config.Title
|
||||||
|
"Page" "error"
|
||||||
|
"User" ""}}
|
||||||
|
<div class="hero is-fullheight-with-navbar is-light">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container has-text-centered">
|
||||||
|
<p class="title">Error <span class="has-text-danger">{{.StatusCode}}</span></p>
|
||||||
|
<p class="subtitle">{{.Reason}}</p>
|
||||||
|
<a class="button is-danger" href="/">Go to homepage</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
39
resources/templates/home.html
Normal file
39
resources/templates/home.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
{{template "base.html"}}
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{template "head" dict
|
||||||
|
"SiteTitle" .Config.Title
|
||||||
|
"PageTitle" "Home"
|
||||||
|
"Theme" .Config.Theme}}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{ template "navbar" dict
|
||||||
|
"SiteTitle" .Config.Title
|
||||||
|
"Page" "home"
|
||||||
|
"User" .Username }}
|
||||||
|
<div class="hero is-fullheight-with-navbar is-light">
|
||||||
|
<div class="hero-head" style="margin: 10px 30px 0 30px">
|
||||||
|
<div class="row columns is-multiline is-mobile">
|
||||||
|
{{if .User.ShowPublic}}
|
||||||
|
{{ range $_, $card := .Config.Users._public_.Cards }}
|
||||||
|
<div class="column is-half-tablet is-one-quarter-fullhd is-one-third-desktop is-full-mobile">
|
||||||
|
<div class="card is-flex is-flex-direction-column" style="min-height: 175px; max-height: 175px; overflow: auto">
|
||||||
|
{{dyn_template $card.Type $card}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{ range $_, $card := .User.Cards }}
|
||||||
|
<div class="column is-half-tablet is-one-quarter-fullhd is-one-third-desktop is-full-mobile">
|
||||||
|
<div class="card is-flex is-flex-direction-column" style="min-height: 175px; max-height: 175px; overflow: auto">
|
||||||
|
{{dyn_template $card.Type $card}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
33
resources/templates/login.html
Normal file
33
resources/templates/login.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
{{template "base.html"}}
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{template "head" dict
|
||||||
|
"SiteTitle" .Config.Title
|
||||||
|
"PageTitle" "Login"
|
||||||
|
"Theme" .Config.Theme}}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{ template "navbar" dict "SiteTitle" .Config.Title "Page" "login" "User" .Username}}
|
||||||
|
<div class="hero is-fullheight-with-navbar is-light">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container has-text-centered">
|
||||||
|
<article style="max-width: 30rem; margin-left: auto; margin-right: auto" class="message is-danger">
|
||||||
|
{{if eq .Error "usr"}}
|
||||||
|
<p class="message-header">User does not exist!</p>
|
||||||
|
{{else if eq .Error "pwd"}}
|
||||||
|
<p class="message-header">Incorrect password!</p>
|
||||||
|
{{end}}
|
||||||
|
</article>
|
||||||
|
<p class="subtitle">Login to {{.Config.Title}}</p>
|
||||||
|
<form action="/login" method="post">
|
||||||
|
<input style="max-width: 20rem" class="input is-info" type="text" placeholder="Username" name="username"><br>
|
||||||
|
<br>
|
||||||
|
<input style="max-width: 20rem" class="input is-info" type="password" placeholder="Password" name="password"><br><br>
|
||||||
|
<input class="button" style="background-color: #f5f5f5" type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
220
routes.go
Normal file
220
routes.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create struct to store template context
|
||||||
|
type TemplateData struct {
|
||||||
|
Username string
|
||||||
|
Config Conf
|
||||||
|
User User
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerRoutes(app App) {
|
||||||
|
// Root endpoint, home page
|
||||||
|
app.Route.Path("/").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
// Get session by name from config
|
||||||
|
session, _ := app.Session.Get(req, app.Config.Session.Name)
|
||||||
|
// Attempt to get loggedInAs from session
|
||||||
|
loggedInAs, ok := session.Values["loggedInAs"].(string)
|
||||||
|
// If user not logged in and login is required
|
||||||
|
if !ok && app.Config.LoginRequired {
|
||||||
|
// Redirect to login page
|
||||||
|
http.Redirect(res, req, "/login", http.StatusFound)
|
||||||
|
} else if !ok && !app.Config.LoginRequired {
|
||||||
|
// If not logged in and login is not required
|
||||||
|
// Set logged in to public user
|
||||||
|
loggedInAs = "_public_"
|
||||||
|
// Set logged in user to session
|
||||||
|
session.Values["loggedInAs"] = loggedInAs
|
||||||
|
// Save session
|
||||||
|
_ = session.Save(req, res)
|
||||||
|
}
|
||||||
|
// Create template context
|
||||||
|
tmplData := TemplateData{Username: loggedInAs, Config: app.Config, User: app.Config.Users[loggedInAs]}
|
||||||
|
// Execute home template with provided context
|
||||||
|
err := app.Templates["home"].Execute(res, tmplData)
|
||||||
|
if err != nil {
|
||||||
|
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Error executing home template")
|
||||||
|
Log.Warn().Err(err).Msg("Error executing home template")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// /login endpoint, login page
|
||||||
|
app.Route.Path("/login").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
// Check request method
|
||||||
|
switch req.Method {
|
||||||
|
// If GET request
|
||||||
|
case http.MethodGet:
|
||||||
|
// Get session by name from config
|
||||||
|
session, _ := app.Session.Get(req, app.Config.Session.Name)
|
||||||
|
// Attempt to get loggedInAs from session
|
||||||
|
loggedInAs, ok := session.Values["loggedInAs"].(string)
|
||||||
|
// If logged in and not as public user
|
||||||
|
if ok && loggedInAs != "_public_" {
|
||||||
|
// Redirect back to home page
|
||||||
|
http.Redirect(res, req, "/", http.StatusFound)
|
||||||
|
}
|
||||||
|
// Get query parameter error
|
||||||
|
urlErr := req.URL.Query().Get("error")
|
||||||
|
// Execute login template with provided context
|
||||||
|
err := app.Templates["login"].Execute(res, TemplateData{Config: app.Config, Error: urlErr, Username: loggedInAs})
|
||||||
|
if err != nil {
|
||||||
|
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Error executing login template")
|
||||||
|
Log.Warn().Err(err).Msg("Error executing login template")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If POST request
|
||||||
|
case http.MethodPost:
|
||||||
|
// Parse form in POST request body
|
||||||
|
_ = req.ParseForm()
|
||||||
|
// Get password from form
|
||||||
|
password := req.PostForm.Get("password")
|
||||||
|
// Get username from form
|
||||||
|
username := req.PostForm.Get("username")
|
||||||
|
// Get user from config by username
|
||||||
|
user, ok := app.Config.Users[username]
|
||||||
|
// If user not found
|
||||||
|
if !ok {
|
||||||
|
// Redirect to login page with error parameter set to usr
|
||||||
|
http.Redirect(res, req, "/login?error=usr", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Compare hash stored in config to password in form
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
|
||||||
|
// If password was incorrect
|
||||||
|
if err != nil {
|
||||||
|
// Redirect to login page with error parameter set to pwd
|
||||||
|
http.Redirect(res, req, "/login?error=pwd", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Get session by name from config
|
||||||
|
session, _ := app.Session.Get(req, app.Config.Session.Name)
|
||||||
|
// Set loggedInAs value in session to username from form
|
||||||
|
session.Values["loggedInAs"] = username
|
||||||
|
// Save session
|
||||||
|
err = session.Save(req, res)
|
||||||
|
if err != nil {
|
||||||
|
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Error saving session")
|
||||||
|
Log.Warn().Err(err).Msg("Error saving session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Redirect to homepage
|
||||||
|
http.Redirect(res, req, "/", http.StatusFound)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// /logout endpoint, logout and redirect
|
||||||
|
app.Route.Path("/logout").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
// Get session by name from config
|
||||||
|
session, _ := app.Session.Get(req, app.Config.Session.Name)
|
||||||
|
// Remove loggedInAs value from session
|
||||||
|
delete(session.Values, "loggedInAs")
|
||||||
|
// Save session
|
||||||
|
err := session.Save(req, res)
|
||||||
|
if err != nil {
|
||||||
|
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Error saving session")
|
||||||
|
Log.Warn().Err(err).Msg("Error while handling error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Redirect to login page
|
||||||
|
http.Redirect(res, req, "/login", http.StatusFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
// /status/:b64url endpoint, return status of base64 encoded URL as JSON
|
||||||
|
app.Route.Path("/status/{b64url}").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
// Get path variables
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
// Create JSON encoder writing to response
|
||||||
|
enc := json.NewEncoder(res)
|
||||||
|
// Decode base64 URL string
|
||||||
|
url, _ := base64.StdEncoding.DecodeString(vars["b64url"])
|
||||||
|
// Create new HEAD request to check status without downloading whole page
|
||||||
|
headReq, err := http.NewRequest(http.MethodHead, string(url), nil)
|
||||||
|
if err != nil {
|
||||||
|
// Encode down status and write to response
|
||||||
|
_ = enc.Encode(map[string]interface{}{"code": 0, "down": true})
|
||||||
|
Log.Warn().Err(err).Msg("Error creating HEAD request for status check")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Create new HTTP client with 5 second timeout
|
||||||
|
client := http.Client{Timeout: 5 * time.Second}
|
||||||
|
// Use client to do request created above
|
||||||
|
headRes, err := client.Do(headReq)
|
||||||
|
if err != nil {
|
||||||
|
// Encode down status and write to response
|
||||||
|
_ = enc.Encode(map[string]interface{}{"code": 0, "down": true})
|
||||||
|
Log.Warn().Err(err).Msg("Error executing HEAD request for status check")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Encode returned status and write to response
|
||||||
|
_ = enc.Encode(map[string]interface{}{"code": headRes.StatusCode, "down": false})
|
||||||
|
})
|
||||||
|
|
||||||
|
// /proxy/:b64url endpoint, proxy HTTP connection bypassing CORS
|
||||||
|
app.Route.Path("/proxy/{b64url}").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
// Get path variables
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
// Decode base64 URL string
|
||||||
|
url, _ := base64.StdEncoding.DecodeString(vars["b64url"])
|
||||||
|
// If URL is allowed for proxy
|
||||||
|
if strSlcContains(app.Config.AllowProxy, string(url)) {
|
||||||
|
// Create new HTTP request with the same parameters as sent to endpoint
|
||||||
|
proxyReq, err := http.NewRequest(req.Method, string(url), req.Body)
|
||||||
|
if err != nil {
|
||||||
|
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Proxying connection failed")
|
||||||
|
Log.Warn().Err(err).Msg("Error creating request for proxy")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Create new HTTP client with 5 second timeout
|
||||||
|
client := http.Client{Timeout: 5 * time.Second}
|
||||||
|
// Use client to do request created above
|
||||||
|
proxyRes, err := client.Do(proxyReq)
|
||||||
|
if err != nil {
|
||||||
|
httpError(res, app.Templates["error"], app.Config, http.StatusInternalServerError, "Proxying connection failed")
|
||||||
|
Log.Warn().Err(err).Msg("Error executing request for proxy")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Close proxy response body at end of function
|
||||||
|
defer proxyRes.Body.Close()
|
||||||
|
// Copy data from proxy response to response
|
||||||
|
io.Copy(res, proxyRes.Body)
|
||||||
|
} else {
|
||||||
|
httpError(res, app.Templates["error"], app.Config, http.StatusBadRequest, "This url is not in allowedProxy")
|
||||||
|
Log.Warn().Str("url", string(url)).Msg("URL is disallowed for proxy")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Catch-all route, gzip-compressing file server
|
||||||
|
app.Route.PathPrefix("/").HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
// Create OS-specific path to requested file
|
||||||
|
filePath := filepath.Join("resources", "public", req.URL.Path)
|
||||||
|
// Open requested file
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
Log.Warn().Str("file", filePath).Msg("File not found")
|
||||||
|
httpError(res, app.Templates["error"], app.Config, http.StatusNotFound, "This file was not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Close file at end of function
|
||||||
|
defer file.Close()
|
||||||
|
// Compress response
|
||||||
|
gzRes := compressRes(res)
|
||||||
|
// Close compressed response at end of function
|
||||||
|
defer gzRes.Close()
|
||||||
|
// Copy file contents to compressed response
|
||||||
|
io.Copy(gzRes, file)
|
||||||
|
})
|
||||||
|
}
|
45
simpledash-sample.toml
Normal file
45
simpledash-sample.toml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
title = "SimpleDash"
|
||||||
|
theme = "dark"
|
||||||
|
loginRequired = false
|
||||||
|
allowProxy = ["https://www.metaweather.com/"]
|
||||||
|
|
||||||
|
[session]
|
||||||
|
name = "simpledash-session"
|
||||||
|
|
||||||
|
[users]
|
||||||
|
[[users._public_.card]]
|
||||||
|
type = "weather"
|
||||||
|
title = "Weather"
|
||||||
|
data = {"woeid" = "2442047"}
|
||||||
|
|
||||||
|
[users.admin]
|
||||||
|
passwordHash = "$2a$10$w00dzQ1PP6nwXLhuzV2pFOUU6m8bcZXtDX3UVxpOYq3fTSwVMqPge"
|
||||||
|
showPublic = true
|
||||||
|
|
||||||
|
[[users.admin.card]]
|
||||||
|
type = "status"
|
||||||
|
title = "Google"
|
||||||
|
icon = "ion:logo-google"
|
||||||
|
desc = "Google search engine. Status card example."
|
||||||
|
url = "https://www.google.com"
|
||||||
|
|
||||||
|
[[users.admin.card]]
|
||||||
|
type = "simple"
|
||||||
|
title = "Gmail"
|
||||||
|
icon = "simple-icons:gmail"
|
||||||
|
desc = "Gmail mail client. Simple card example"
|
||||||
|
url = "http://openwrt/"
|
||||||
|
|
||||||
|
[[users.admin.card]]
|
||||||
|
type = "collection"
|
||||||
|
title = "Programming"
|
||||||
|
icon = "entypo:code"
|
||||||
|
[users.admin.card.data]
|
||||||
|
Godoc = {"url" = "https://pkg.go.dev", "target" = "newTab"}
|
||||||
|
Ruby-Doc = {"url" = "https://ruby-doc.org/", "target" = "sameTab"}
|
||||||
|
|
||||||
|
[[users.admin.card]]
|
||||||
|
type = "collection"
|
||||||
|
title = "Science"
|
||||||
|
icon = "ic:outline-science"
|
||||||
|
data = {"Google Scholar" = {"url" = "https://robinhood.com/", "target" = "sameTab"}}
|
38
template.go
Normal file
38
template.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Function to dynamically execute template and return results
|
||||||
|
func dynamicTemplate(name string, data interface{}) (template.HTML, error) {
|
||||||
|
// Create new buffer
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
// Execute template writing to buffer with provided data
|
||||||
|
err := templates[name].Execute(buf, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
// Return results of template execution
|
||||||
|
return template.HTML(buf.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap URL with proxy
|
||||||
|
func wrapProxy(url string) string {
|
||||||
|
// Encode URL with base64
|
||||||
|
b64url := base64.StdEncoding.EncodeToString([]byte(url))
|
||||||
|
// Return /proxy/{url}
|
||||||
|
return fmt.Sprint("/proxy/", b64url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get template function map
|
||||||
|
func getFuncMap() template.FuncMap {
|
||||||
|
// Return function map with template functions
|
||||||
|
return template.FuncMap{
|
||||||
|
"dyn_template": dynamicTemplate,
|
||||||
|
"proxy": wrapProxy,
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user