20 changed files with 5806 additions and 0 deletions
@ -0,0 +1,4 @@ |
|||
/simpledash |
|||
/simpledash.toml |
|||
/sessions.db |
|||
.idea/ |
@ -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"` |
|||
} |
@ -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 |
|||
} |
@ -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 |
|||
) |
@ -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= |
@ -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") |
|||
} |
|||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
File diff suppressed because one or more lines are too long
@ -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}} |
@ -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> |
@ -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}} |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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) |
|||
}) |
|||
} |
@ -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"}} |
@ -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