Initial Commit
This commit is contained in:
		
							
								
								
									
										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