Initial Commit

This commit is contained in:
2021-03-25 22:25:10 -07:00
parent 32e120ddaa
commit c6af685621
20 changed files with 5806 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
/simpledash
/simpledash.toml
/sessions.db
.idea/
+33
View 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
View 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
View 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
View 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
View 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")
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+65
View 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
View 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
View File
@@ -0,0 +1,18 @@
<div class="card-header">
<a class="card-header-title" href="{{.URL}}">
{{if ne .Icon ""}}
{{template "icon" .Icon}}&nbsp;
{{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
View File
@@ -0,0 +1,39 @@
<div class="card-header">
<a class="card-header-title" href="{{.URL}}">
{{if ne .Icon ""}}
{{template "icon" .Icon}}&nbsp;
{{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
View 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&nbsp;<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) + " &deg;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) + " &deg;F"
document.getElementById('weatherMaxText').innerHTML = "Max: " + round(data["consolidated_weather"][0]["max_temp"]*1.8+32) + " &deg;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
View 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
View 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
View 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
View 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
View 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
View 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,
}
}