diff --git a/assets.go b/assets.go new file mode 100644 index 0000000..d28486d --- /dev/null +++ b/assets.go @@ -0,0 +1,428 @@ +// Code generated by "esc -o assets.go assets templates"; DO NOT EDIT. + +package main + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "io/ioutil" + "net/http" + "os" + "path" + "sync" + "time" +) + +type _escLocalFS struct{} + +var _escLocal _escLocalFS + +type _escStaticFS struct{} + +var _escStatic _escStaticFS + +type _escDirectory struct { + fs http.FileSystem + name string +} + +type _escFile struct { + compressed string + size int64 + modtime int64 + local string + isDir bool + + once sync.Once + data []byte + name string +} + +func (_escLocalFS) Open(name string) (http.File, error) { + f, present := _escData[path.Clean(name)] + if !present { + return nil, os.ErrNotExist + } + return os.Open(f.local) +} + +func (_escStaticFS) prepare(name string) (*_escFile, error) { + f, present := _escData[path.Clean(name)] + if !present { + return nil, os.ErrNotExist + } + var err error + f.once.Do(func() { + f.name = path.Base(name) + if f.size == 0 { + return + } + var gr *gzip.Reader + b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(f.compressed)) + gr, err = gzip.NewReader(b64) + if err != nil { + return + } + f.data, err = ioutil.ReadAll(gr) + }) + if err != nil { + return nil, err + } + return f, nil +} + +func (fs _escStaticFS) Open(name string) (http.File, error) { + f, err := fs.prepare(name) + if err != nil { + return nil, err + } + return f.File() +} + +func (dir _escDirectory) Open(name string) (http.File, error) { + return dir.fs.Open(dir.name + name) +} + +func (f *_escFile) File() (http.File, error) { + type httpFile struct { + *bytes.Reader + *_escFile + } + return &httpFile{ + Reader: bytes.NewReader(f.data), + _escFile: f, + }, nil +} + +func (f *_escFile) Close() error { + return nil +} + +func (f *_escFile) Readdir(count int) ([]os.FileInfo, error) { + return nil, nil +} + +func (f *_escFile) Stat() (os.FileInfo, error) { + return f, nil +} + +func (f *_escFile) Name() string { + return f.name +} + +func (f *_escFile) Size() int64 { + return f.size +} + +func (f *_escFile) Mode() os.FileMode { + return 0 +} + +func (f *_escFile) ModTime() time.Time { + return time.Unix(f.modtime, 0) +} + +func (f *_escFile) IsDir() bool { + return f.isDir +} + +func (f *_escFile) Sys() interface{} { + return f +} + +// FS returns a http.Filesystem for the embedded assets. If useLocal is true, +// the filesystem's contents are instead used. +func FS(useLocal bool) http.FileSystem { + if useLocal { + return _escLocal + } + return _escStatic +} + +// Dir returns a http.Filesystem for the embedded assets on a given prefix dir. +// If useLocal is true, the filesystem's contents are instead used. +func Dir(useLocal bool, name string) http.FileSystem { + if useLocal { + return _escDirectory{fs: _escLocal, name: name} + } + return _escDirectory{fs: _escStatic, name: name} +} + +// FSByte returns the named file from the embedded assets. If useLocal is +// true, the filesystem's contents are instead used. +func FSByte(useLocal bool, name string) ([]byte, error) { + if useLocal { + f, err := _escLocal.Open(name) + if err != nil { + return nil, err + } + b, err := ioutil.ReadAll(f) + _ = f.Close() + return b, err + } + f, err := _escStatic.prepare(name) + if err != nil { + return nil, err + } + return f.data, nil +} + +// FSMustByte is the same as FSByte, but panics if name is not present. +func FSMustByte(useLocal bool, name string) []byte { + b, err := FSByte(useLocal, name) + if err != nil { + panic(err) + } + return b +} + +// FSString is the string version of FSByte. +func FSString(useLocal bool, name string) (string, error) { + b, err := FSByte(useLocal, name) + return string(b), err +} + +// FSMustString is the string version of FSMustByte. +func FSMustString(useLocal bool, name string) string { + return string(FSMustByte(useLocal, name)) +} + +var _escData = map[string]*_escFile{ + + "/assets/css/main.css": { + local: "assets/css/main.css", + size: 4980, + modtime: 1547133837, + compressed: ` +H4sIAAAAAAAC/7RXW2vcOBR+n18hCIWkVK4nzTSJA2GhF/Zh96mPpQ+yJdsismRkeTJpyX9fdLMlW54u +CwsJjHUuOpfvXNSqjoFfOwA6ymFLaNOqAuzz/M3D7nW3KwV+MdRKMCELcHF7e/uwxYwMpyInBTGphESK +Cl4ALjgxdEyPWSW4IlwZzh5hTHlTgP2hP6W1TheXDFVPk5YWsdqoeKZYtQU4OAuyliBMpPUHyYZyyEit +CpBbco3gtSHWgis40J+kANeks8Re0g7J2N08v70jtxEdlo1hKVH11EgxcgxT3ERKISNdnz/neZ4H1DOa +Al7tr+h1KBGrKWE48G62fkd5P6qspozEsc0OpAPZR80GAKZDz9BLAShnlBNYMqGjCkApJCayAPv+BAbB +KAYXVVVZygkOLcLiWUsNRIHccH3oT+ACYzxLQ4kwHYcC3OhsvnqTJOJPlDfQfMVJs1lfSB/C08iiT58+ +PXiAIUYbXoCKcEWkvS3rxJFAWgluwz7KQceSD1ASnWrDddERPoZWeJx53N04q3oxUItfSRhS9EgeolQV +4GJ/v7/b3+njZ1I+UQXFkciaiWc4VFIwZuKvxFi1wdVZP0oC9U+owUq5xcBGZlJiXGiWp1RZxlnfL5I+ +ZXujRAF4bqkicOhRRfThs0R9aIO5vhyVciFeGB2HDZWDYKMyp0r0BXCBlTbO7stVqWHYx2fSNwJ76lw1 +Jj6c7TPOTqGU6KJULxBnL53w5TQv3NUNC1HumsrksY9Y2t/YR3OPx1Wi2D2Otm5+HNlWnwhETV8Ukv7U +YsxB0MhNsDwVwB7bgumaTLVjV3JELWPQe30mTquGrM+i2tERn/RsxWvC3lJdwqkvX74EYLZpus5dU8nc +cdzlDp5qsBt1yJgEB4rJsB4QhxT0FqJHIhWtEAuljXGxsAVeIM2QbMhy7OzvPLmkDVybvZ8c1gbOk9U1 +Pn2oyZkxdUU2p4ZuO+SKIWicjgUiB5cUGwDe+TXFW4xGJYxChUpG/MVKrxCPCp+zwAr4xDreVW4NF1C1 +ENbfdBXbKWGAojum7VZwnsZnhu3cTr9+/brWoUTTMAJrKoelARYw0wiL24zFQ4rlNXlBRzF2U3wxGuP+ +aIELo3npDtfhSF3E0NoRLX/ejxTHf7EoMqnAdNDpxVs5crPff97f37t+KTBicFpTFyW499hxfLoTMmRZ +j3SgJWVUvRSgpRgTvt3P3RIZtHP3e2OJ8N/pUvoJKcfkZNjydA+UTYku83fA/WWHq4Qbj5geFz3jjf4P +DLvzPdsz5P3JFWoyzAb4yeXLmxoj0g/n1Sq/UemYHi9IV9KmIZzgb5UkhH9rXUH7ja0X1Idqy8Qzqh5p +1/yK5lQ4uVyGpsmlp2Y9MjYY+eUUfJPI8e83nBgtSQiEPXPKh8fFPs/3QYXUQnbAbM/ffZX8KLhQl9/V +S09+XGX+VNcSNLD27WMzwf7zcDjom+ymbuaUDDemm9xPoT86gikCl/qJNhFJd2W456V6vWut4xmGsKYn +Yp4Qr7tJz2rNTKxdK/Zp7VgIUE4VRewhss0ZYpSslsWUCwmDF2nWlWF7jjuwyoP36KJLTtoNo4EhQ0Pr +WMP36c1ZK0LgpT2cdAdv0t/MwWgSOsgsdA1jVZFh2NZ2fX2P6vttba+73fu34Fun1w5bvQN4+94DzdUj +4hhczpX84aAfNldutTza+tCpl4JBfX8PGCoJy/yZ+XI2rtcnbQYAmeDsBc5LWhJunm3o/JoUsGFSo5HN +ChVBHewQRw3pCFcOy8PajsiMDanHcE7GSLKrj33lTQFVSCq9RiFlg/sOzK+CrQDPRf0/RjgZujnCIR7+ +nC1WAvxNMB27f2/7AjTg5s54BOwNTttvIBe0ubuEzo83ocq/bONcqwz1BCL/BAAA///ACu/RdBMAAA== +`, + }, + + "/assets/vendor/css/grids-responsive-min.css": { + local: "assets/vendor/css/grids-responsive-min.css", + size: 8032, + modtime: 1547133812, + compressed: ` +H4sIAAAAAAAC/8SVT4/bRg/G7/4UziHA+wa1LM9oRrL3UjTNoUBQFMipR68trIXqjyHLSbaL/e6FIc1G +fEjOIqc9PhQ1Esnh81t/eLf469qXy69p4hOz+NidH/vq4TQsTbqxy7/3p657t/hcHcr2Uh6X1/ZY9svh +VC5/+/L7cgoni9MwnC+79fqhGk7X++TQNevH25vr87Uv1/d1d79u9peh7Nef//j46c8vn5LmuPiwXvza +lMdqv7wc+rJsl/v2uPxfU7Wrb9VxOO2sS1zZ/P8puR2yuq4uzWrzy1wwaahGmVFtqYSnjkpPZUFkimez +f2FfNxiwGMgw4DDgMZBjoMDAFgKGaUslaYPBQg3WZdiBWJfBuizTIB2VpPV4WEazHcwBW+gw3VNJvoXt +zuHwnD0nr+MsbqN4OlaXc71/3FVtXbXl6r7uDv/c/dt1zW5zV5fDUPary3l/qNqHXdv1zb6++9b1R4x9 +LfuhOuzr1b6uHtrd0J3vhvL7sOrL27reEvfXoXuGbXga1yxLNt7n758jq2R+ZBeJtdZCdoHznJI3JnGQ +6nF6IdUnnv+Gm56alDxwP94zaVLwH8pwbiGb/k4+e7KV2mBxgFO2tbwNcDO3s+wc2mBeCstoYcxJbhc4 +pG6EDs3n6KROGOY6U7qDL8/G5qQrkQm3P+QXUjdCiR4+xHewCJnsunhuTyFXui+zeebSvSi41YR0+PBs +erlwM7KX6gpaneED9CFTuDPMP/OXZhR4aRhUzGyaW+FymNlIt9LliMCUubR7qXiTpu+fn6Pkzoo5t5vj +7OybYNJQjTKj2lIJTx2VnsqCyBTPZv/Cvm4wYDGQYcBhwGMgx0CBgS0EDNOWStIGg4UarMuwA7Eug3VZ +pkE6Kknr8bCMZjuYA7bQYbqnknwL253D4Tl7Tl7HWbwdt6dtULgtrFKE23wbVG7zPYpxe1xCgdvTGDVu +811WuD1NTOM2t4gYt/nNVLk9rpXAbclJYtwOvqJxW/BAhdvBfzRuS3YU4/a4tgK3JRvTuC05XITbwf80 +bkt2KHM7+KTG7dFmBG5LDhnhtuSfGrclqES5HaxX4/YrMGUu/VPc9tmc2/XD7OybYNJQjTKj2lIJTx2V +nsqCyBTPZv/Cvm4wYDGQYcBhwGMgx0CBgS0EDNOWStIGg4UarMuwA7Eug3VZpkE6Kknr8bCMZjuYA7bQ +YbqnknwL253D4Tl7Tl7HWbwdt6dtULgtrFKE23wbVG7zPYpxe1xCgdvTGDVu811WuD1NTOM2t4gYt/nN +VLk9rpXAbclJYtwOvqJxW/BAhdvBfzRuS3YU4/a4tgK3JRvTuC05XITbwf80bkt2KHM7+KTG7dFmBG5L +DhnhtuSfGrclqES5HaxX4/YrMGUu/VPcLtI5t7/Xs7NvgklDNcqMakslPHVUeioLIlM8m/0L+7rBgMVA +hgGHAY+BHAMFBrYQMExbKkkbDBZqsC7DDsS6DNZlmQbpqCStx8Mymu1gDthCh+meSvItbHcOh+fsOXkd +Z/F23J62QeG2sEoRbvNtULnN9yjG7XEJBW5PY9S4zXdZ4fY0MY3b3CJi3OY3U+X2uFYCtyUniXE7+IrG +bcEDFW4H/9G4LdlRjNvj2grclmxM47bkcBFuB//TuC3Zoczt4JMat0ebEbgtOWSE25J/atyWoBLldrBe +jduvwJS5NHL7vwAAAP//TZMIY2AfAAA= +`, + }, + + "/assets/vendor/css/pure-min.css": { + local: "assets/vendor/css/pure-min.css", + size: 16449, + modtime: 1547133812, + compressed: ` +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 +`, + }, + + "/templates/footer.html": { + local: "templates/footer.html", + size: 228, + modtime: 1547128347, + compressed: ` +H4sIAAAAAAAC/1zPwQqDMAzG8btPEXIX0XPsu9SlYCFbh0kLvv3Yplh76uH/ox+hgWNxHXEsEHnGZ2Iv +fSphE7+j6wDoBwAAaB0rY9EkoKNhHc88VVnzconpEPeVJfH+zddA07NZeikesXEP8aozbkGD9e8sUn11 +XvV/PgEAAP///4iBjuQAAAA= +`, + }, + + "/templates/header.html": { + local: "templates/header.html", + size: 200, + modtime: 1547136888, + compressed: ` +H4sIAAAAAAAC/0yNsQrDIBRFd7/ikj0JZLYuhW6d2h+w+lIFayGaLPL+PRgkZHzvcM6V1m8wQad06+ag +k4OhmGlBKcOj3vfKmDslcP6elJL+ErOQo/WbEteK+cdMMbdOr0NoLvyM4bV+3j4HArMArp4jbWnpfxTX +QwCkm1Qpp8IsRzdV1FaPKEVbW3sAAAD//7XiV5HIAAAA +`, + }, + + "/templates/htmlfooter.html": { + local: "templates/htmlfooter.html", + size: 115, + modtime: 1547128255, + compressed: ` +H4sIAAAAAAAC/1JQUFCw0U/JLLPjAjGrqxWKEvPSUxVUMnUUVMoUrGwV9IKTizILSooVamvBSmyKwXyF +4qJkW6XqapCq2lolOxt9iDjcnNS8FIgWG/2k/JRKOy4b/YyS3Bw7LkAAAAD//zPK1EBzAAAA +`, + }, + + "/templates/htmlheader.html": { + local: "templates/htmlheader.html", + size: 801, + modtime: 1547128244, + compressed: ` +H4sIAAAAAAAC/5yST2/UMBDF7/spplaPdQxCCKjivZRKcAGk9oKqHrz2ZD2LYwd7krKK8t1R9g+70J44 +JeOZ95uXF9cXH7/e3H//dgue27Bc1PsHQO3RuPkFoG6RDVhvckHWoudGvhfnrWha1GIgfOpSZgE2RcbI +WjyRY68dDmRR7ooroEhMJshiTUD9unr1AsphsZk6phTPaH8NeuZO4s+eBi1ujPUo57mcwpngS5K71lEY +KP6AjEEL03UBJafeekl23uIzNlooUwpyUdSuVWOGuVV1cS2Atx1qQa1Zo5oPnhGLT5ltz/CfuD2PiQMu +x7G6I8bqfq6mCSTMJ/3qUNdqP7ZY1BdSPlADgeHzLXx4PLjahwclWy3mnK6Vsi5uSmVD6l0TTMbKplaZ +jfmlAq2Kmv/62+JpUG+qd6eq2hSxrNWet1zUFw8YHTWPUh4MjyNkE9cIl3QFlwNca6jueBuweEQuME3P +cvrTPYY0jrNymg6ZjiNgdEflSws+oXGY73amTivOP/qEPJn/l12r4w2vV8ltD9E5GoCcFsFsU89iufgd +AAD//1puW/YhAwAA +`, + }, + + "/templates/login.html": { + local: "templates/login.html", + size: 812, + modtime: 1547136935, + compressed: ` +H4sIAAAAAAAC/6SSTW7jMAyF9zkFwb3hC9gGZjHLwQSTuYBs0bFQSRQoKU1uX8RW/LNogaIr88mPTx8F +NtrcYLAqxhYnZUcYyCeSSlmL3QmgGVncyxCyUDUfrFWlrLl60ghqSIZ9i3WOJLXlq/EIjtLEusXz38v/ +Oe+ZaMjqSGmRAHuEOXdgn4RtdRXOAV82gMaqniyMLC2SU8Zi9/v5gV9aC8XY1LNh12B8yAmMfvnBK0er +SI9ALSa6J4Rg1UATW03S4iEVQeXEIw85rsS1Nrfu9BP+oGJ8Z9HYnUv1Ff3qLgNseplh04c5zusl3wKP +nzALOXI9CR6bJhreer7vmg7sW9fCvumFfeuHf+UX/KEN4PgsZYIi+pwS+xIUc+9MOsIVw66ughin5IHd +ZfY39XJelrPeb2dTP1e8O5VLPwIAAP//0xnP9ywDAAA= +`, + }, + + "/templates/user_dashboard.html": { + local: "templates/user_dashboard.html", + size: 0, + modtime: 1547138339, + compressed: ` +H4sIAAAAAAAC/wEAAP//AAAAAAAAAAA= +`, + }, + + "/": { + isDir: true, + local: "", + }, + + "/assets": { + isDir: true, + local: "assets", + }, + + "/assets/css": { + isDir: true, + local: "assets/css", + }, + + "/assets/vendor": { + isDir: true, + local: "assets/vendor", + }, + + "/assets/vendor/css": { + isDir: true, + local: "assets/vendor/css", + }, + + "/templates": { + isDir: true, + local: "templates", + }, +} diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..5c23d9c --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,355 @@ +html { + min-height: 100%; +} + +body { + color: #777; + min-height: 100%; +} + +a { + text-decoration: none; +} + +div.content { + padding: 15px; + min-height: 100%; + color: black; +} + +div.half { + width: 50%; +} + +.header { + margin-left: 0; +} + +.fa-2 { + font-size: 2em; +} + +.primary { + color: #0078e7; +} + +.primary-bg { + background-color: #0078e7; +} + +.error { + color: #DD0000; +} + +.error-bg { + background-color: #DD0000; +} + +div.optionalfield { + margin: 2em; +} + + +input.file { + padding: .5em .6em; + display: inline-block; + border: 1px solid #ccc; + box-shadow: inset 0 1px 3px #ddd; + border-radius: 4px; +} + +input.ranking-input { + width: 50px; + border-radius: 5px; + border: 1px solid #CCC; + text-align: center; +} + +i.move-icon { + cursor: ns-resize; +} + +#menu { + width: 100%; + height: 40px; + position: relative; + background: #191818; + webkit-overflow-scrolling: touch; +} + +#menu .pure-menu-heading { + display: inline-block; +} + +#menu .pure-menu-nonlink { + color: #777; + padding: .5em 1em; + display: block; + text-decoration: none; + white-space: nowrap; +} + +#menu .menu-button { + display: inline; + position: absolute; + top: 0px; + right: 0px; + margin-top: 10px; + margin-right: 10px; + color: white; + text-decoration: none; +} + +.menu-bottom { + width: 150px; + border-top: 1px solid white; +} + +#menu .menu-container { + display: none; + position: absolute; + right: 0px; + top: 40px; + background-color: #191818; +} + +#menu .menu-container>ul { + background-color: #191818; +} + +div.horizontal-scroll { + overflow-x: scroll; +} + +img.thumbnail { + height: 100px; + max-height: 100%; + max-width: 100%; +} + +.thumbnail-container { + display: block; + height: 100%; + background-color: #EEE; + padding-top: 20px; +} + +.padding { + padding: 5px; +} + +.space { + margin: 5px; +} + +.space-sides { + margin-left: 5px; + margin-right: 5px; +} + +.space-vertical { + margin-top: 5px; + margin-bottom: 5px; +} + +.large { + font-size: 18px; +} + +.big-space { + margin: 10px; +} + +.left { + text-align: left; +} +.right { + text-align: right; +} +.center { + text-align: center; +} + +.center-all { + text-align: center; + vertical-align: center; + margin: auto; +} + +table.center tbody>td { + text-align: center; +} + +table.padding td { + padding: 5px; +} + +table tfoot { + border-top: 1px solid #CCC; +} + +.pure-button-error { + background-color: #DD0000; + color: #FFF; +} + +.pure-button-toggle-first { + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; +} +.pure-button-toggle-middle { + border-radius: 0px; + margin-left: -5px; + border-left: 1px solid #CCC; +} +.pure-button-toggle-last { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + margin-left: -5px; + border-left: 1px solid #CCC; +} + +.pure-button:disabled { + background-color: #CCC; + color: #999; +} + +#modal-body { + margin-bottom: 15px; +} + +#modal-overlay { + visibility: hidden; + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: 100%; + text-align: center; + z-index: 1000; + background-color: rgba(0, 0, 0, 0.5); +} + +#modal-overlay>div { + margin: 10% 10%; + width: 80%; + margin: 100px auto; + background-color: #FFF; + border: 1px solid #000; + border-radius: 10px; + padding: 15px; + text-align: center; +} + +div#embiggenedScreenShot { + cursor: pointer; + background-color: #FFF; +} + +div#embiggenedScreenShot>img{ + max-width:100%; + max-height:100%; +} + +div.fullscreen { + height: 100%; + width: 100%; + position: absolute; + top: 0px; + left: 0px; + text-align: center; + margin: auto auto; + z-index:1001; +} + +.pure-form input[disabled]:not([type]).disabled-but-visible { + background-color: #FFF; + color: #555; +} +input.larger { + width: 400px; +} + +@media (min-width: 40em) { + #menu { + width: 150px; + height: 100%; + position: fixed; + } + + #menu .menu-button { + display: none; + } + + #menu .menu-container{ + display: initial; + width: 100%; + } + + .menu-bottom { + width: 150px; + position: fixed; + left: 0px; + bottom: 0px; + } + + .header { + margin-left: 150px; + } + + div.flash { + font-size: 24px; + position: fixed; + top: 0px; + width: 100%; + } + + div.flash.error { + background-color: #DD0000; + color: #FFFFFF; + } + + div.flash.success { + background-color: #229af9; + color: #FFFFFF; + } +} + +/* Small Screens */ +@media screen and (max-width:35.5em) { + div.pure-control-group label.control-label { + text-align: left; + } + .only-large { + display: none; + } + .only-small { + display: default; + } + .team-management-buttons { + text-align:left; + } + .team-management-buttons>.pure-button { + margin-top: 1em; + } +} + +/* Starting at Small, horizontal */ +@media screen and (min-width: 35.5em) { + div.pure-control-group label.control-label { + text-align: left; + } + .only-small { + display: none; + } +} + +/* Small Horizontal to Medium */ +@media screen and (min-width: 35.5em) and (max-width: 48em) { } + +/* Medium Screens */ +@media screen and (min-width: 48em) and (max-width:64em) { } + +/* Larger Screens */ +@media (min-width: 64em) { } + diff --git a/assets/vendor/css/grids-responsive-min.css b/assets/vendor/css/grids-responsive-min.css new file mode 100644 index 0000000..c25d7a4 --- /dev/null +++ b/assets/vendor/css/grids-responsive-min.css @@ -0,0 +1,7 @@ +/*! +Pure v0.6.2 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/yahoo/pure/blob/master/LICENSE.md +*/ +@media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-12,.pure-u-sm-1-2,.pure-u-sm-1-24,.pure-u-sm-1-3,.pure-u-sm-1-4,.pure-u-sm-1-5,.pure-u-sm-1-6,.pure-u-sm-1-8,.pure-u-sm-10-24,.pure-u-sm-11-12,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-2-24,.pure-u-sm-2-3,.pure-u-sm-2-5,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24,.pure-u-sm-3-24,.pure-u-sm-3-4,.pure-u-sm-3-5,.pure-u-sm-3-8,.pure-u-sm-4-24,.pure-u-sm-4-5,.pure-u-sm-5-12,.pure-u-sm-5-24,.pure-u-sm-5-5,.pure-u-sm-5-6,.pure-u-sm-5-8,.pure-u-sm-6-24,.pure-u-sm-7-12,.pure-u-sm-7-24,.pure-u-sm-7-8,.pure-u-sm-8-24,.pure-u-sm-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-sm-1-24{width:4.1667%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%}.pure-u-sm-1-5{width:20%}.pure-u-sm-5-24{width:20.8333%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%}.pure-u-sm-7-24{width:29.1667%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%}.pure-u-sm-2-5{width:40%}.pure-u-sm-10-24,.pure-u-sm-5-12{width:41.6667%}.pure-u-sm-11-24{width:45.8333%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%}.pure-u-sm-13-24{width:54.1667%}.pure-u-sm-14-24,.pure-u-sm-7-12{width:58.3333%}.pure-u-sm-3-5{width:60%}.pure-u-sm-15-24,.pure-u-sm-5-8{width:62.5%}.pure-u-sm-16-24,.pure-u-sm-2-3{width:66.6667%}.pure-u-sm-17-24{width:70.8333%}.pure-u-sm-18-24,.pure-u-sm-3-4{width:75%}.pure-u-sm-19-24{width:79.1667%}.pure-u-sm-4-5{width:80%}.pure-u-sm-20-24,.pure-u-sm-5-6{width:83.3333%}.pure-u-sm-21-24,.pure-u-sm-7-8{width:87.5%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%}.pure-u-sm-23-24{width:95.8333%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-24-24,.pure-u-sm-5-5{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-12,.pure-u-md-1-2,.pure-u-md-1-24,.pure-u-md-1-3,.pure-u-md-1-4,.pure-u-md-1-5,.pure-u-md-1-6,.pure-u-md-1-8,.pure-u-md-10-24,.pure-u-md-11-12,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-2-24,.pure-u-md-2-3,.pure-u-md-2-5,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24,.pure-u-md-3-24,.pure-u-md-3-4,.pure-u-md-3-5,.pure-u-md-3-8,.pure-u-md-4-24,.pure-u-md-4-5,.pure-u-md-5-12,.pure-u-md-5-24,.pure-u-md-5-5,.pure-u-md-5-6,.pure-u-md-5-8,.pure-u-md-6-24,.pure-u-md-7-12,.pure-u-md-7-24,.pure-u-md-7-8,.pure-u-md-8-24,.pure-u-md-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-24{width:4.1667%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%}.pure-u-md-1-5{width:20%}.pure-u-md-5-24{width:20.8333%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%}.pure-u-md-7-24{width:29.1667%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%}.pure-u-md-2-5{width:40%}.pure-u-md-10-24,.pure-u-md-5-12{width:41.6667%}.pure-u-md-11-24{width:45.8333%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%}.pure-u-md-13-24{width:54.1667%}.pure-u-md-14-24,.pure-u-md-7-12{width:58.3333%}.pure-u-md-3-5{width:60%}.pure-u-md-15-24,.pure-u-md-5-8{width:62.5%}.pure-u-md-16-24,.pure-u-md-2-3{width:66.6667%}.pure-u-md-17-24{width:70.8333%}.pure-u-md-18-24,.pure-u-md-3-4{width:75%}.pure-u-md-19-24{width:79.1667%}.pure-u-md-4-5{width:80%}.pure-u-md-20-24,.pure-u-md-5-6{width:83.3333%}.pure-u-md-21-24,.pure-u-md-7-8{width:87.5%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%}.pure-u-md-23-24{width:95.8333%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-24-24,.pure-u-md-5-5{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-12,.pure-u-lg-1-2,.pure-u-lg-1-24,.pure-u-lg-1-3,.pure-u-lg-1-4,.pure-u-lg-1-5,.pure-u-lg-1-6,.pure-u-lg-1-8,.pure-u-lg-10-24,.pure-u-lg-11-12,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-2-24,.pure-u-lg-2-3,.pure-u-lg-2-5,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24,.pure-u-lg-3-24,.pure-u-lg-3-4,.pure-u-lg-3-5,.pure-u-lg-3-8,.pure-u-lg-4-24,.pure-u-lg-4-5,.pure-u-lg-5-12,.pure-u-lg-5-24,.pure-u-lg-5-5,.pure-u-lg-5-6,.pure-u-lg-5-8,.pure-u-lg-6-24,.pure-u-lg-7-12,.pure-u-lg-7-24,.pure-u-lg-7-8,.pure-u-lg-8-24,.pure-u-lg-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-lg-1-24{width:4.1667%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%}.pure-u-lg-1-5{width:20%}.pure-u-lg-5-24{width:20.8333%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%}.pure-u-lg-7-24{width:29.1667%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%}.pure-u-lg-2-5{width:40%}.pure-u-lg-10-24,.pure-u-lg-5-12{width:41.6667%}.pure-u-lg-11-24{width:45.8333%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%}.pure-u-lg-13-24{width:54.1667%}.pure-u-lg-14-24,.pure-u-lg-7-12{width:58.3333%}.pure-u-lg-3-5{width:60%}.pure-u-lg-15-24,.pure-u-lg-5-8{width:62.5%}.pure-u-lg-16-24,.pure-u-lg-2-3{width:66.6667%}.pure-u-lg-17-24{width:70.8333%}.pure-u-lg-18-24,.pure-u-lg-3-4{width:75%}.pure-u-lg-19-24{width:79.1667%}.pure-u-lg-4-5{width:80%}.pure-u-lg-20-24,.pure-u-lg-5-6{width:83.3333%}.pure-u-lg-21-24,.pure-u-lg-7-8{width:87.5%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%}.pure-u-lg-23-24{width:95.8333%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-24-24,.pure-u-lg-5-5{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-12,.pure-u-xl-1-2,.pure-u-xl-1-24,.pure-u-xl-1-3,.pure-u-xl-1-4,.pure-u-xl-1-5,.pure-u-xl-1-6,.pure-u-xl-1-8,.pure-u-xl-10-24,.pure-u-xl-11-12,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-2-24,.pure-u-xl-2-3,.pure-u-xl-2-5,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24,.pure-u-xl-3-24,.pure-u-xl-3-4,.pure-u-xl-3-5,.pure-u-xl-3-8,.pure-u-xl-4-24,.pure-u-xl-4-5,.pure-u-xl-5-12,.pure-u-xl-5-24,.pure-u-xl-5-5,.pure-u-xl-5-6,.pure-u-xl-5-8,.pure-u-xl-6-24,.pure-u-xl-7-12,.pure-u-xl-7-24,.pure-u-xl-7-8,.pure-u-xl-8-24,.pure-u-xl-9-24{display:inline-block;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xl-1-24{width:4.1667%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%}.pure-u-xl-1-5{width:20%}.pure-u-xl-5-24{width:20.8333%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%}.pure-u-xl-7-24{width:29.1667%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%}.pure-u-xl-2-5{width:40%}.pure-u-xl-10-24,.pure-u-xl-5-12{width:41.6667%}.pure-u-xl-11-24{width:45.8333%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%}.pure-u-xl-13-24{width:54.1667%}.pure-u-xl-14-24,.pure-u-xl-7-12{width:58.3333%}.pure-u-xl-3-5{width:60%}.pure-u-xl-15-24,.pure-u-xl-5-8{width:62.5%}.pure-u-xl-16-24,.pure-u-xl-2-3{width:66.6667%}.pure-u-xl-17-24{width:70.8333%}.pure-u-xl-18-24,.pure-u-xl-3-4{width:75%}.pure-u-xl-19-24{width:79.1667%}.pure-u-xl-4-5{width:80%}.pure-u-xl-20-24,.pure-u-xl-5-6{width:83.3333%}.pure-u-xl-21-24,.pure-u-xl-7-8{width:87.5%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%}.pure-u-xl-23-24{width:95.8333%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-24-24,.pure-u-xl-5-5{width:100%}} \ No newline at end of file diff --git a/assets/vendor/css/pure-min.css b/assets/vendor/css/pure-min.css new file mode 100644 index 0000000..2ce0e15 --- /dev/null +++ b/assets/vendor/css/pure-min.css @@ -0,0 +1,11 @@ +/*! +Pure v0.6.2 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/yahoo/pure/blob/master/LICENSE.md +*/ +/*! +normalize.css v^3.0 | MIT License | git.io/normalize +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */.pure-button:focus,a:active,a:hover{outline:0}.pure-table,table{border-collapse:collapse;border-spacing:0}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}abbr[title]{border-bottom:1px dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}.pure-button,input{line-height:normal}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}.pure-button,.pure-form input:not([type]),.pure-menu{box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend,td,th{padding:0}legend{border:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-align-content:flex-start;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u,.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;display:inline-block;zoom:1}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;zoom:1;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-.43em}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:transparent;background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{filter:alpha(opacity=90);background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000\9}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto;margin:0;border-radius:0;border-right:1px solid #111;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=tel],.pure-form input[type=color],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=text],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px}.pure-form input[type=color]{padding:.2em .5em}.pure-form input:not([type]):focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=text]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=checkbox]:focus,.pure-form input[type=radio]:focus{outline:#129FEA auto 1px}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input:not([type])[disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=text][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input:not([type]),.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=text],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-aligned .pure-help-inline,.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=tel],.pure-form input[type=color],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=text],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=tel],.pure-group input[type=color],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=text]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-disabled,.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}.pure-table{empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td,.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} \ No newline at end of file diff --git a/buildplugins.sh b/buildplugins.sh new file mode 100755 index 0000000..98c36ec --- /dev/null +++ b/buildplugins.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +cd plugin_src +for i in `ls`; do + go build -buildmode=plugin $i +done +cd .. +mv plugin_src/*.so plugins/ diff --git a/endpoints_api_comics.go b/endpoints_api_comics.go deleted file mode 100644 index 6b8a54c..0000000 --- a/endpoints_api_comics.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/gorilla/mux" -) - -func handleApiComicsCall(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "application/json") - - vars := mux.Vars(req) - cid, cidok := vars["cid"] - fn, fnok := vars["function"] - switch req.Method { - case "GET": - if !cidok { - handleApiListComics(w) - return - } - if !fnok { - handleApiShowComic(cid, w) - return - } - switch fn { - case "search": - handleApiSearchComic(cid, w) - return - - default: - userError(w) - return - } - - default: - userError(w) - return - } -} - -func handleApiListComics(w http.ResponseWriter) { - var res []byte - var err error - if res, err = json.Marshal(m.Comics); err != nil { - serverError(w) - return - } - fmt.Fprint(w, string(res)) -} - -func handleApiSearchComic(nm string, w http.ResponseWriter) { - nm = strings.ToLower(nm) - var cs []Comic - for _, c := range m.Comics { - if strings.Contains(strings.ToLower(c.Name), nm) { - cs = append(cs, c) - } - } - var res []byte - var err error - if res, err = json.Marshal(cs); err != nil { - serverError(w) - return - } - fmt.Fprint(w, string(res)) -} - -func handleApiShowComic(cid string, w http.ResponseWriter) { - var err error - var c *Comic - pts := strings.Split(cid, ";") - if len(pts) != 2 { - userError(w) - return - } - if c, err = m.GetComic(pts[0], pts[1]); err != nil { - userError(w) - return - } - - var res []byte - if res, err = json.Marshal(c); err != nil { - userError(w) - return - } - fmt.Fprint(w, string(res)) - return -} diff --git a/endpoints_api_users.go b/endpoints_api_users.go index 25a38e5..51dba74 100644 --- a/endpoints_api_users.go +++ b/endpoints_api_users.go @@ -125,7 +125,7 @@ func handleApiSubUser(uid string, slug string, w http.ResponseWriter) { userError(w) return } - _, err = m.GetComic(pts[0], pts[1]) + //_, err = m.GetComic(pts[0], pts[1]) if err != nil { userError(w) return diff --git a/endpoints_public.go b/endpoints_public.go index 07c32f7..8b0bf6d 100644 --- a/endpoints_public.go +++ b/endpoints_public.go @@ -8,31 +8,30 @@ import ( ) func handleRequest(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) + page := initPageData(w, req) - var fOk, uidOk bool - var f, uid string - f, fOk = vars["function"] - uid, uidOk = vars["uid"] - if !fOk || !uidOk { - // Not sure what you want me to do here, Hoss. - http.Error(w, "You did a bad", 400) - return - } - switch f { - case "rss": - handleRssFeed(uid, w) - default: - http.Error(w, "You did a bad", 400) + //vars := mux.Vars(req) + //var fOk, uidOk bool + //var f, uid string + //f, fOk = vars["function"] + //uid, uidOk = vars[""] + if !page.LoggedIn { + handleUserLoginForm(page) } } -func handleRssFeed(uid string, w http.ResponseWriter) { +func handleRssFeed(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + var uid string + var uidOk bool + if uid, uidOk = vars["uid"]; !uidOk { + userError(w) + return + } w.Header().Set("Content-Type", "application/xml") - v, err := buildRssFeed(uid) if err != nil { - http.Error(w, err.Error(), 400) + userError(w) return } fmt.Fprint(w, v) diff --git a/endpoints_user.go b/endpoints_user.go new file mode 100644 index 0000000..5e3f249 --- /dev/null +++ b/endpoints_user.go @@ -0,0 +1,69 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + "strings" + + "github.com/gorilla/mux" +) + +func handleUserRequest(w http.ResponseWriter, req *http.Request) { + page := initPageData(w, req) + + vars := mux.Vars(req) + f, fOk := vars["function"] + if fOk && f == "login" { + handleUserLogin(page) + return + } + + if !page.LoggedIn { + page.SubTitle = "ribbit login" + page.show("login.html", w) + return + } + page.SubTitle = "dashboard" + page.show("user_dashboard.html", w) +} + +func handleUserLoginForm(page *pageData) { + page.SubTitle = "ribbit login" + page.show("login.html", page.session.w) +} + +func handleUserLogin(page *pageData) { + var err error + var u *User + if page.LoggedIn { + redirect("/", page.session.w, page.session.req) + } + email := page.session.req.FormValue("email") + + if u, err = m.GetUserByEmail(email); err != nil { + page.session.setFlashMessage("Invalid Login", "error") + redirect("/", page.session.w, page.session.req) + return + } + + password := page.session.req.FormValue("password") + fmt.Println("Hitting func") + if err := doLogin(u.Uuid, password); err != nil { + fmt.Println("Login Failure: ", err.Error()) + page.session.setFlashMessage("Invalid Login", "error") + } else { + page.session.setStringValue("id", u.Uuid) + } + redirect("/", page.session.w, page.session.req) +} + +// doLogin attempts to log in with the given email/password +// If it can't, it returns an error +func doLogin(uid, password string) error { + fmt.Println("Doing Login", uid, password) + if strings.TrimSpace(uid) != "" && strings.TrimSpace(password) != "" { + return m.checkCredentials(uid, password) + } + return errors.New("Invalid Credentials") +} diff --git a/helper_xkcd.go b/helper_xkcd.go deleted file mode 100644 index 48f3e6e..0000000 --- a/helper_xkcd.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/PuerkitoBio/goquery" -) - -func downloadXKCDList() []Comic { - var ret []Comic - ret = append(ret, *NewComic("xkcd", "XKCD", "Randall Munroe", "xkcd")) - return ret -} - -func getXKCDRssItem(slug string) (string, error) { - desc, err := getXKCDFeedDesc(time.Now()) - if err != nil { - return "", err - } - comic, err := m.GetComic(SRC_XKCD, slug) - if err != nil { - return "", err - } - desc = "" - ret := " \n" - ret += " " + comic.Name + "\n" - ret += " " + comic.LastUpdate.Format(time.RFC1123Z) + "\n" - ret += " xkcd;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "\n" - ret += " " + getXKCDComicUrl(time.Now()) + "\n" - ret += " " + desc + "\n" - ret += " \n" - return ret, nil -} - -func getXKCDComicUrl(date time.Time) string { - // TODO: Actually make this work correctly - // Get the previous comic number - // and find the next one - return fmt.Sprintf( - "http://xkcd.com/", - ) -} - -func getXKCDFeedDesc(date time.Time) (string, error) { - res, err := http.Get(getXKCDComicUrl(date)) - if err != nil { - return "", err - } - defer res.Body.Close() - if res.StatusCode != 200 { - return "", errors.New(fmt.Sprintf("Status code error: %d %s", res.StatusCode, res.Status)) - } - - // Load the HTML document - doc, err := goquery.NewDocumentFromReader(res.Body) - if err != nil { - return "", err - } - // Find the Picture - sel := doc.Find("div#comic>img") - src, exists := sel.Attr("src") - if !exists { - return "", errors.New("Couldn't find image source") - } - src = "https:" + src - title, exists := sel.Attr("title") - if !exists { - title = "" - } - return "

" + title + "

", nil -} diff --git a/helpers.go b/helpers.go index 281aec0..9a5a049 100644 --- a/helpers.go +++ b/helpers.go @@ -1,98 +1,9 @@ package main import ( - "errors" "net/http" - "strings" - "time" ) -const ( - SRC_GOCOMICS = "gocomics" - SRC_DILBERT = "dilbert" - SRC_XKCD = "xkcd" -) - -func downloadComicsList() []Comic { - var ret []Comic - ret = append(ret, downloadGoComicsList()...) - ret = append(ret, downloadDilbertList()...) - ret = append(ret, downloadXKCDList()...) - return ret -} - -func getRssItem(source, slug string) (string, error) { - switch source { - case SRC_GOCOMICS: - return getGoComicsRssItem(slug) - case SRC_DILBERT: - return getDilbertRssItem(slug) - case SRC_XKCD: - return getXKCDRssItem(slug) - } - return "", errors.New("Invalid source") -} - -func getComicUrl(source, slug string, dt time.Time) (string, error) { - switch source { - case SRC_GOCOMICS: - return getGoComicsComicUrl(slug, dt), nil - case SRC_DILBERT: - return getDilbertComicUrl(dt), nil - case SRC_XKCD: - return getXKCDComicUrl(dt), nil - } - return "", errors.New("Invalid source") -} - -func getComicDesc(source, slug string, dt time.Time) (string, error) { - switch source { - case SRC_GOCOMICS: - return getGoComicsFeedDesc(slug, dt) - case SRC_DILBERT: - return getDilbertFeedDesc(dt) - case SRC_XKCD: - return getXKCDFeedDesc(dt) - } - return "", errors.New("Unknown Comic Source") -} - -func buildRssFeed(uid string) (string, error) { - var usr *User - var err error - if usr, err = m.GetUser(uid); err != nil { - return "", err - } - output := []string{ - "", - "", - " ", - " BCW Comic Feed", - " http://ribbit.bullercodeworks.com/edit/" + uid + "", - " Comic feed for " + usr.Username + "", - " en-us", - " " + time.Now().Format(time.RFC1123) + "", - " 40", - } - - //date := time.Now() - for _, slug := range usr.SubSlugs { - pts := strings.Split(slug, ";") - if len(pts) != 2 { - continue - } - if comic, err := m.GetComic(pts[0], pts[1]); err == nil { - output = append(output, comic.GetRssItem()) - } - } - - output = append(output, []string{ - " ", - "", - }...) - return strings.Join(output, "\n"), nil -} - func addStringIfUnique(st string, sl []string) []string { for i := range sl { if sl[i] == st { diff --git a/main.go b/main.go index d63d05c..9098cf1 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,11 @@ package main +//go:generate esc -o assets.go assets templates + import ( + "bufio" "fmt" + "html/template" "log" "net/http" "os" @@ -9,17 +13,42 @@ import ( "strconv" "strings" "syscall" - "time" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/justinas/alice" + "golang.org/x/crypto/ssh/terminal" ) const AppName = "ribbit" const DbName = AppName + ".db" +// pageData is stuff that changes per request +type pageData struct { + Site *SiteData + Title string + SubTitle string + Stylesheets []string + HeaderScripts []string + Scripts []string + FlashMessage string + FlashClass string + LoggedIn bool + IsAdmin bool + Menu []menuItem + BottomMenu []menuItem + session *pageSession + + TemplateData interface{} +} + +type menuItem struct { + Label string + Location string + Icon string +} + var sessionStore *sessions.CookieStore var r *mux.Router var m *model @@ -32,106 +61,59 @@ func main() { errorExit("Unable to initialize Model: " + err.Error()) } - if len(os.Args) > 2 { - key, val := os.Args[1], os.Args[2] - switch key { - case "--add-user-sub": - if len(os.Args) < 3 { - errorExit("Usage: --add-user-sub \nFor a list of slugs, use --list-comics") - } - slug := os.Args[3] - var u *User - if u, err = m.GetUserByName(val); err != nil { - errorExit("Couldn't find a user with the username " + val) - } - pts := strings.Split(slug, ";") - if len(pts) != 2 { - errorExit("Invalid slug given: " + slug) - } - _, err := m.GetComic(pts[0], pts[1]) - if err != nil { - errorExit("Couldn't find comic with slug: " + slug) - } - fmt.Println(u.SubSlugs) - fmt.Println(slug) - u.SubSlugs = append(u.SubSlugs, slug) - fmt.Println(u.SubSlugs) - m.SaveUser(u) - done() - default: - errorExit("Unknown argument") - } - } else if len(os.Args) > 1 { - switch os.Args[1] { - case "--test": - fmt.Println(buildRssFeed("30f14e57-6500-443c-8c77-f352788eacb0")) - done() - case "--list-comics": - comics := m.GetAllComics() - for _, c := range comics { - fmt.Printf("[ %s;%s ] %s\n", c.Source, c.Slug, c.Name) - } - done() - case "--update-feeds": - fmt.Println("Updating User Feeds...") - m.UpdateAllUserFeeds() - fmt.Println("Done.") - done() - case "--update-comics": - fmt.Println("Updating the Comics List...") - comics := downloadComicsList() - for _, c := range comics { - fmt.Printf("Updating [ %s - %s, %s ]\n", c.Slug, c.Name, c.Artist) - m.SaveComic(&c) - } - m.saveChanges() - fmt.Println("Done.") + initialize() + sessionStore = sessions.NewCookieStore([]byte(m.Site.sessionSecret)) - default: - errorExit("Unknown argument") + for _, arg := range os.Args { + switch arg { + case "-dev": + m.Site.DevMode = true + fmt.Println("Running in Dev Mode") } } r = mux.NewRouter() r.StrictSlash(true) - //r.PathPrefix("/assets/").Handler(http.FileServer()) + r.PathPrefix("/assets/").Handler(http.FileServer(FS(m.Site.DevMode))) pub := r.PathPrefix("/").Subrouter() - pub.HandleFunc("/", handleRequest) - pub.HandleFunc("/api", handleApiCall) - pub.HandleFunc("/api/users", handleApiUsersCall) - pub.HandleFunc("/api/users/{uid}", handleApiUsersCall) - pub.HandleFunc("/api/users/{uid}/{function}", handleApiUsersCall) - pub.HandleFunc("/api/users/{uid}/{function}/{slug}", handleApiUsersCall) - pub.HandleFunc("/api/comics", handleApiComicsCall) - pub.HandleFunc("/api/comics/{cid}", handleApiComicsCall) - pub.HandleFunc("/api/comics/{cid}/{function}", handleApiComicsCall) - pub.HandleFunc("/{function}", handleRequest) - pub.HandleFunc("/{function}/{uid}", handleRequest) - pub.HandleFunc("/{function}/{uid}/{subfunc}", handleRequest) - pub.HandleFunc("/{function}/{uid}/{subfunc}/{slug}", handleRequest) + pub.HandleFunc("/", handleUserRequest) + /* + pub.HandleFunc("/api", handleApiCall) + pub.HandleFunc("/api/users", handleApiUsersCall) + pub.HandleFunc("/api/users/{uid}", handleApiUsersCall) + pub.HandleFunc("/api/users/{uid}/{function}", handleApiUsersCall) + pub.HandleFunc("/api/users/{uid}/{function}/{slug}", handleApiUsersCall) + pub.HandleFunc("/api/comics", handleApiComicsCall) + pub.HandleFunc("/api/comics/{cid}", handleApiComicsCall) + pub.HandleFunc("/api/comics/{cid}/{function}", handleApiComicsCall) + */ + pub.HandleFunc("/rss/{uid}", handleRssFeed) + pub.HandleFunc("/user/{function}", handleUserRequest) http.Handle("/", r) chain := alice.New(loggingHandler).Then(r) // Refresh the DB at 2 AM - go func() { - for { - if m.Site.LastSave.IsZero() || (time.Now().Day() != m.Site.LastSave.Day() && time.Now().Hour() == 2) { - fmt.Println("Updating GoComics List...") - comics := downloadComicsList() - for _, c := range comics { - fmt.Printf("Updating [ %s - %s, %s ]\n", c.Slug, c.Name, c.Artist) - m.SaveComic(&c) + /* + go func() { + for { + if m.Site.LastSave.IsZero() || (time.Now().Day() != m.Site.LastSave.Day() && time.Now().Hour() == 2) { + fmt.Println("Updating GoComics List...") + comics := downloadComicsList() + for _, c := range comics { + fmt.Printf("Updating [ %s - %s, %s ]\n", c.Slug, c.Name, c.Artist) + m.SaveComic(&c) + } + fmt.Println("Updating User Feeds...") + m.UpdateAllUserFeeds() + m.saveChanges() + fmt.Println("Done.") } - fmt.Println("Updating User Feeds...") - m.UpdateAllUserFeeds() - m.saveChanges() - fmt.Println("Done.") + time.Sleep(time.Minute) } - time.Sleep(time.Minute) - } - }() + }() + */ // Set up a channel to intercept Ctrl+C for graceful shutdowns c := make(chan os.Signal, 1) @@ -148,10 +130,97 @@ func main() { log.Fatal(http.ListenAndServe("127.0.0.1:"+strconv.Itoa(m.Site.Port), chain)) } +func buildRssFeed(uid string) (string, error) { + return uid, nil +} + func loggingHandler(h http.Handler) http.Handler { return handlers.LoggingHandler(os.Stdout, h) } +func initPageData(w http.ResponseWriter, req *http.Request) *pageData { + if m.Site.DevMode { + w.Header().Set("Cache-Control", "no-cache") + } + p := new(pageData) + + // Get session + var err error + var s *sessions.Session + if s, err = sessionStore.Get(req, m.Site.sessionSecret); err != nil { + fmt.Println("Session error... Recreating.") + http.Error(w, err.Error(), 500) + } + p.session = new(pageSession) + p.session.session = s + p.session.req = req + p.session.w = w + + // First check if we're logged in + userId, _ := p.session.getStringValue("id") + // With a valid account + user, err := m.GetUser(userId) + if err == nil { + p.LoggedIn = true + p.IsAdmin = user.IsAdmin + } + + p.Site = m.Site + p.SubTitle = "ribbit" + p.Stylesheets = make([]string, 0, 0) + p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/pure-min.css") + p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/grids-responsive-min.css") + p.Stylesheets = append(p.Stylesheets, "/assets/css/main.css") + + p.HeaderScripts = make([]string, 0, 0) + + p.Scripts = make([]string, 0, 0) + + p.FlashMessage, p.FlashClass = p.session.getFlashMessage() + if p.FlashClass == "" { + p.FlashClass = "hidden" + } + + // Build the menu + if p.LoggedIn { + p.BottomMenu = append(p.BottomMenu, menuItem{"Logout", "/user/logout", "fa-sign-out"}) + } else { + if p.IsAdmin { + p.BottomMenu = append(p.BottomMenu, menuItem{"Admin", "/admin", "fa-sign-in"}) + } + } + + return p +} + +func (p *pageData) show(tmplName string, w http.ResponseWriter) error { + for _, tmpl := range []string{ + "htmlheader.html", + "header.html", + tmplName, + "footer.html", + "htmlfooter.html", + } { + if err := outputTemplate(tmpl, p, w); err != nil { + fmt.Printf("%s\n", err) + return err + } + } + return nil +} + +func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error { + n := "/templates/" + tmplName + l := template.Must(template.New("layout").Parse(FSMustString(m.Site.DevMode, n))) + t := template.Must(l.Parse(FSMustString(m.Site.DevMode, n))) + return t.Execute(w, tmplData) +} + +// redirect can be used only for GET redirects +func redirect(url string, w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, url, 303) +} + func done() { os.Exit(0) } @@ -166,3 +235,41 @@ func assertError(err error) { panic(err) } } + +func initialize() { + // Check if we have an admin user already + if !m.hasAdminUser() { + // Nope, create one + reader := bufio.NewReader(os.Stdin) + fmt.Println("Create new Admin user") + fmt.Print("Email: ") + email, _ := reader.ReadString('\n') + email = strings.TrimSpace(email) + u := NewUser(email) + var pw1, pw2 []byte + for string(pw1) != string(pw2) || string(pw1) == "" { + fmt.Print("Password: ") + pw1, _ = terminal.ReadPassword(0) + fmt.Println("") + fmt.Print("Repeat Password: ") + pw2, _ = terminal.ReadPassword(0) + fmt.Println("") + if string(pw1) != string(pw2) { + fmt.Println("Entered Passwords don't match!") + } + } + u.IsAdmin = true + m.SaveUser(u) + assertError(m.updateUserPassword(u.Uuid, string(pw1))) + } + + if m.Site.sessionSecret == "" { + reader := bufio.NewReader(os.Stdin) + fmt.Println("A good session secret is like a good password") + fmt.Print("Create New Session Secret: ") + sessSc, _ := reader.ReadString('\n') + sessSc = strings.TrimSpace(sessSc) + m.Site.sessionSecret = sessSc + assertError(m.Site.SaveToDB()) + } +} diff --git a/model.go b/model.go index 985a4d4..acce94a 100644 --- a/model.go +++ b/model.go @@ -13,8 +13,8 @@ type model struct { bolt *boltease.DB dbFileName string - Users []User - Comics []Comic + Users []User + Feeds []Feed Site *SiteData } @@ -31,11 +31,14 @@ func NewModel() (*model, error) { if err = m.initDB(); err != nil { return nil, errors.New("Unable to initialzie DB: " + err.Error()) } - m.LoadSiteData() + m.Site = NewSiteData(m) + if err = m.Site.LoadFromDB(); err != nil { + return nil, err + } if err = m.LoadUsers(); err != nil { return nil, err } - if err = m.LoadComics(); err != nil { + if err = m.LoadFeeds(); err != nil { return nil, err } @@ -55,16 +58,15 @@ func (m *model) initDB() error { if err = m.bolt.MkBucketPath([]string{"users"}); err != nil { return err } - if err = m.bolt.MkBucketPath([]string{"comics"}); err != nil { + if err = m.bolt.MkBucketPath([]string{"feeds"}); err != nil { return err } return nil } func (m *model) saveChanges() { - m.Site.LastSave = time.Now() - m.SaveSite() - //m.SaveAllComics(m.Comics) + m.Site.SaveToDB() + //m.SaveAllFeeds(m.Feeds) m.SaveAllUsers(m.Users) } @@ -76,14 +78,14 @@ func (m *model) UpdateAllUserFeeds() { allSubs = addStringIfUnique(sub, allSubs) } } - // So we have allSubs which contains all subscribed comics for all users + // So we have allSubs which contains all subscribed feeds for all users for _, sub := range allSubs { - fmt.Println("Updating Comic: " + sub) + fmt.Println("Updating Feed: " + sub) pts := strings.Split(sub, ";") if len(pts) != 2 { continue } - c, err := m.GetComic(pts[0], pts[1]) + c, err := m.GetFeed(pts[0], pts[1]) if err != nil { fmt.Println(sub, ":", err) continue @@ -92,7 +94,7 @@ func (m *model) UpdateAllUserFeeds() { fmt.Println(sub, ":", err.Error()) continue } - if err = m.SaveComic(c); err != nil { + if err = m.SaveFeed(c); err != nil { fmt.Println(sub, ":", err.Error()) continue } @@ -100,7 +102,7 @@ func (m *model) UpdateAllUserFeeds() { } type Source interface { - downloadList() []Comic + downloadList() []Feed getRssItem(slug string) (string, error) getUrl(slug string, dt time.Time) (string, error) getDescription(slug string, dt time.Time) (string, error) diff --git a/model_comics.go b/model_comics.go deleted file mode 100644 index 3d5ce61..0000000 --- a/model_comics.go +++ /dev/null @@ -1,202 +0,0 @@ -package main - -import ( - "errors" - "time" -) - -type Comic struct { - Name string - Artist string - Slug string - Source string - Desc string - LastUpdate time.Time -} - -func NewComic(s, n, a, source string) *Comic { - return &Comic{ - Name: n, - Artist: a, - Slug: s, - Source: source, - } -} - -func (c *Comic) GetBucket() []string { - return []string{"comics", c.Source, c.Slug} -} - -func (c *Comic) Update() error { - dt := time.Now() - desc, err := getComicDesc(c.Source, c.Slug, dt) - if err != nil { - return err - } - if desc == c.Desc { - return errors.New("Comic didn't change") - } - c.Desc = desc - c.LastUpdate = dt - return nil -} - -func (c *Comic) GetUrl(dt time.Time) string { - var v string - var e error - if v, e = getComicUrl(c.Source, c.Slug, dt); e != nil { - return "" - } - return v -} - -func (c *Comic) GetDesc(dt time.Time) string { - var v string - var e error - if v, e = getComicDesc(c.Source, c.Slug, dt); e != nil { - return "" - } - return v -} - -func (c *Comic) GetRssItem() string { - var v string - var e error - if v, e = getRssItem(c.Source, c.Slug); e != nil { - return "" - } - return v -} - -// DB Function to save a comic -func (m *model) SaveComic(c *Comic) error { - var err error - if err = m.bolt.OpenDB(); err != nil { - return err - } - defer m.bolt.CloseDB() - bkt := c.GetBucket() - if err = m.bolt.MkBucketPath(bkt); err != nil { - return err - } - if err = m.bolt.SetValue(bkt, "name", c.Name); err != nil { - return err - } - if err = m.bolt.SetValue(bkt, "artist", c.Artist); err != nil { - return err - } - if err = m.bolt.SetValue(bkt, "desc", c.Desc); err != nil { - return err - } - if err = m.bolt.SetTimestamp(bkt, "lastupdate", c.LastUpdate); err != nil { - return err - } - // Add it to the cached list - for i, v := range m.Comics { - if v.Source == c.Source && v.Slug == c.Slug { - m.Comics[i] = *c - return nil - } - } - m.Comics = append(m.Comics, *c) - return nil -} - -// DB Function to get a comic -func (m *model) GetComic(source, slug string) (*Comic, error) { - var err error - if err = m.bolt.OpenDB(); err != nil { - return nil, err - } - defer m.bolt.CloseDB() - ret := new(Comic) - ret.Source = source - ret.Slug = slug - bkt := ret.GetBucket() - if ret.Name, err = m.bolt.GetValue(bkt, "name"); err != nil { - return nil, err - } - if ret.Artist, err = m.bolt.GetValue(bkt, "artist"); err != nil { - return nil, err - } - if ret.Desc, err = m.bolt.GetValue(bkt, "desc"); err != nil { - return nil, err - } - if ret.LastUpdate, err = m.bolt.GetTimestamp(bkt, "lastupdate"); err != nil { - return nil, err - } - return ret, nil -} - -// Load all comics into the model -func (m *model) LoadComics() error { - m.Comics = m.GetAllComics() - return nil -} - -// Save all comics to the DB -func (m *model) SaveAllComics(comics []Comic) { - var err error - if err = m.bolt.OpenDB(); err != nil { - return - } - defer m.bolt.CloseDB() - for i := range comics { - m.SaveComic(&comics[i]) - } -} - -// Get all comics from the db -func (m *model) GetAllComics() []Comic { - var ret []Comic - var err error - if err = m.bolt.OpenDB(); err != nil { - return ret - } - defer m.bolt.CloseDB() - var srcs []string - bkt := []string{"comics"} - if srcs, err = m.bolt.GetBucketList(bkt); err != nil { - return ret - } - for _, src := range srcs { - srcBkt := append(bkt, src) - var slugs []string - if slugs, err = m.bolt.GetBucketList(srcBkt); err != nil { - return ret - } - for _, slg := range slugs { - c, err := m.GetComic(src, slg) - if err == nil { - ret = append(ret, *c) - } - } - } - return ret -} - -// Delete a comic from the DB -func (m *model) DeleteComic(slug string) error { - var err error - if err = m.bolt.OpenDB(); err != nil { - return err - } - defer m.bolt.CloseDB() - - return m.bolt.DeleteBucket([]string{"comics"}, slug) -} - -func (m *model) RemoveMissingComics(comics []Comic) { - for _, c := range m.Comics { - var fnd bool - for _, nc := range comics { - if nc.Slug == c.Slug { - fnd = true - break - } - } - if !fnd { - m.DeleteComic(c.Slug) - } - } -} diff --git a/model_feeds.go b/model_feeds.go new file mode 100644 index 0000000..862c472 --- /dev/null +++ b/model_feeds.go @@ -0,0 +1,209 @@ +package main + +import ( + "time" +) + +type Feed struct { + Name string + Author string + Slug string + Source string + Desc string + LastUpdate time.Time +} + +func NewFeed(s, n, a, source string) *Feed { + return &Feed{ + Name: n, + Author: a, + Slug: s, + Source: source, + } +} + +func (f *Feed) GetBucket() []string { + return []string{"feed", f.Source, f.Slug} +} + +func (f *Feed) Update() error { + /* + dt := time.Now() + desc, err := getFeedDesc(f.Source, f.Slug, dt) + if err != nil { + return err + } + if desc == f.Desc { + return errors.New("Feed didn't change") + } + f.Desc = desc + f.LastUpdate = dt + */ + return nil +} + +func (f *Feed) GetUrl(dt time.Time) string { + var v string + /* + var e error + if v, e = getFeedUrl(f.Source, f.Slug, dt); e != nil { + return "" + } + */ + return v +} + +func (f *Feed) GetDesc(dt time.Time) string { + var v string + /* + var e error + if v, e = getFeedDesc(f.Source, f.Slug, dt); e != nil { + return "" + } + */ + return v +} + +func (f *Feed) GetRssItem() string { + var v string + /* + var e error + if v, e = getRssItem(f.Source, f.Slug); e != nil { + return "" + } + */ + return v +} + +// DB Function to save a feed +func (m *model) SaveFeed(f *Feed) error { + var err error + if err = m.bolt.OpenDB(); err != nil { + return err + } + defer m.bolt.CloseDB() + bkt := f.GetBucket() + if err = m.bolt.MkBucketPath(bkt); err != nil { + return err + } + if err = m.bolt.SetValue(bkt, "name", f.Name); err != nil { + return err + } + if err = m.bolt.SetValue(bkt, "author", f.Author); err != nil { + return err + } + if err = m.bolt.SetValue(bkt, "desc", f.Desc); err != nil { + return err + } + if err = m.bolt.SetTimestamp(bkt, "lastupdate", f.LastUpdate); err != nil { + return err + } + // Add it to the cached list + for i, v := range m.Feeds { + if v.Source == f.Source && v.Slug == f.Slug { + m.Feeds[i] = *f + return nil + } + } + m.Feeds = append(m.Feeds, *f) + return nil +} + +// DB Function to get a feed +func (m *model) GetFeed(source, slug string) (*Feed, error) { + var err error + if err = m.bolt.OpenDB(); err != nil { + return nil, err + } + defer m.bolt.CloseDB() + ret := new(Feed) + ret.Source = source + ret.Slug = slug + bkt := ret.GetBucket() + if ret.Name, err = m.bolt.GetValue(bkt, "name"); err != nil { + return nil, err + } + if ret.Author, err = m.bolt.GetValue(bkt, "author"); err != nil { + return nil, err + } + if ret.Desc, err = m.bolt.GetValue(bkt, "desc"); err != nil { + return nil, err + } + if ret.LastUpdate, err = m.bolt.GetTimestamp(bkt, "lastupdate"); err != nil { + return nil, err + } + return ret, nil +} + +// Load all feeds into the model +func (m *model) LoadFeeds() error { + m.Feeds = m.GetAllFeeds() + return nil +} + +// Save all feeds to the DB +func (m *model) SaveAllFeeds(feeds []Feed) { + var err error + if err = m.bolt.OpenDB(); err != nil { + return + } + defer m.bolt.CloseDB() + for i := range feeds { + m.SaveFeed(&feeds[i]) + } +} + +// Get all feeds from the db +func (m *model) GetAllFeeds() []Feed { + var ret []Feed + var err error + if err = m.bolt.OpenDB(); err != nil { + return ret + } + defer m.bolt.CloseDB() + var srcs []string + bkt := []string{"feeds"} + if srcs, err = m.bolt.GetBucketList(bkt); err != nil { + return ret + } + for _, src := range srcs { + srcBkt := append(bkt, src) + var slugs []string + if slugs, err = m.bolt.GetBucketList(srcBkt); err != nil { + return ret + } + for _, slg := range slugs { + c, err := m.GetFeed(src, slg) + if err == nil { + ret = append(ret, *c) + } + } + } + return ret +} + +// Delete a feed from the DB +func (m *model) DeleteFeed(slug string) error { + var err error + if err = m.bolt.OpenDB(); err != nil { + return err + } + defer m.bolt.CloseDB() + + return m.bolt.DeleteBucket([]string{"feeds"}, slug) +} + +func (m *model) RemoveMissingFeeds(feeds []Feed) { + for _, c := range m.Feeds { + var fnd bool + for _, nc := range feeds { + if nc.Slug == c.Slug { + fnd = true + break + } + } + if !fnd { + m.DeleteFeed(c.Slug) + } + } +} diff --git a/model_site.go b/model_site.go deleted file mode 100644 index 5c3d38d..0000000 --- a/model_site.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import "time" - -type SiteData struct { - Title string - Port int - SessionName string - ServerDir string - SessionSecret string - - LastSave time.Time -} - -func NewSiteData() *SiteData { - ret := new(SiteData) - ret.Title = "BCW Comic Feed" - ret.Port = 8080 - ret.SessionName = "bcw-comic-feed" - ret.ServerDir = "./" - return ret -} - -func (m *model) LoadSiteData() { - m.Site = m.GetSite() -} - -func (m *model) SaveSite() error { - var err error - if err = m.bolt.OpenDB(); err != nil { - return err - } - defer m.bolt.CloseDB() - bkt := []string{"site"} - if err = m.bolt.SetValue(bkt, "title", m.Site.Title); err != nil { - return err - } - if err = m.bolt.SetInt(bkt, "port", m.Site.Port); err != nil { - return err - } - if err = m.bolt.SetValue(bkt, "session-name", m.Site.SessionName); err != nil { - return err - } - if err = m.bolt.SetValue(bkt, "session-secret", m.Site.SessionSecret); err != nil { - return err - } - if err = m.bolt.SetValue(bkt, "server-dir", m.Site.ServerDir); err != nil { - return err - } - if err = m.bolt.SetTimestamp(bkt, "last-save", m.Site.LastSave); err != nil { - return err - } - return nil -} - -func (m *model) GetSite() *SiteData { - s := NewSiteData() - var err error - if err = m.bolt.OpenDB(); err != nil { - return s - } - defer m.bolt.CloseDB() - bkt := []string{"site"} - var wrkStr string - var wrkInt int - var wrkTm time.Time - if wrkStr, err = m.bolt.GetValue(bkt, "title"); err == nil { - s.Title = wrkStr - } - if wrkInt, err = m.bolt.GetInt(bkt, "port"); err == nil { - s.Port = wrkInt - } - if wrkStr, err = m.bolt.GetValue(bkt, "session-name"); err == nil { - s.SessionName = wrkStr - } - if wrkStr, err = m.bolt.GetValue(bkt, "session-secret"); err == nil { - s.SessionSecret = wrkStr - } - if wrkStr, err = m.bolt.GetValue(bkt, "server-dir"); err == nil { - s.ServerDir = wrkStr - } - if wrkTm, err = m.bolt.GetTimestamp(bkt, "last-save"); err == nil { - s.LastSave = wrkTm - } - return s -} diff --git a/model_sitedata.go b/model_sitedata.go new file mode 100644 index 0000000..0a8a6b1 --- /dev/null +++ b/model_sitedata.go @@ -0,0 +1,106 @@ +package main + +import ( + "strings" + "time" +) + +/** + * SiteData + * Contains configuration for the website + */ +type SiteData struct { + Title string + Ip string + Port int + SessionName string + ServerDir string + + DevMode bool + + m *model + mPath []string // The path in the db to this site data + changed bool + lastSave time.Time + + sessionSecret string +} + +// NewSiteData returns a SiteData object with the default values +func NewSiteData(m *model) *SiteData { + ret := new(SiteData) + ret.Title = "ribbit" + ret.Ip = "127.0.0.1" + ret.Port = 8080 + ret.SessionName = "good-ol-ribbit" + ret.ServerDir = "./" + ret.mPath = []string{"site"} + ret.m = m + return ret +} + +// load the site data out of the database +// If fields don't exist in the DB, don't clobber what is already in s +func (s *SiteData) LoadFromDB() error { + if err := s.m.bolt.OpenDB(); err != nil { + return err + } + defer s.m.bolt.CloseDB() + + if title, _ := s.m.bolt.GetValue(s.mPath, "title"); strings.TrimSpace(title) != "" { + s.Title = title + } + if ip, err := s.m.bolt.GetValue(s.mPath, "ip"); err == nil { + s.Ip = ip + } + if port, err := s.m.bolt.GetInt(s.mPath, "port"); err == nil { + s.Port = port + } + if sessionName, _ := s.m.bolt.GetValue(s.mPath, "session-name"); strings.TrimSpace(sessionName) != "" { + s.SessionName = sessionName + } + if serverDir, _ := s.m.bolt.GetValue(s.mPath, "server-dir"); strings.TrimSpace(serverDir) != "" { + s.ServerDir = serverDir + } + s.changed = false + if secret, _ := s.m.bolt.GetValue(s.mPath, "session-secret"); strings.TrimSpace(secret) != "" { + s.sessionSecret = secret + } + return nil +} + +// Return if the site data in memory has changed +func (s *SiteData) NeedsSave() bool { + return s.changed +} + +// Save the site data into the DB +func (s *SiteData) SaveToDB() error { + s.lastSave = time.Now() + var err error + if err = s.m.bolt.OpenDB(); err != nil { + return err + } + defer s.m.bolt.CloseDB() + + if err = s.m.bolt.SetValue(s.mPath, "title", s.Title); err != nil { + return err + } + if err = s.m.bolt.SetValue(s.mPath, "ip", s.Ip); err != nil { + return err + } + if err = s.m.bolt.SetInt(s.mPath, "port", s.Port); err != nil { + return err + } + if err = s.m.bolt.SetValue(s.mPath, "session-name", s.SessionName); err != nil { + return err + } + if err = s.m.bolt.SetValue(s.mPath, "server-dir", s.ServerDir); err != nil { + return err + } + s.changed = false + if err = s.m.bolt.SetValue(s.mPath, "session-secret", s.sessionSecret); err != nil { + return err + } + return nil +} diff --git a/model_user.go b/model_user.go index f9da076..eb7d7a5 100644 --- a/model_user.go +++ b/model_user.go @@ -6,12 +6,14 @@ import ( "strings" "github.com/pborman/uuid" + "golang.org/x/crypto/bcrypt" ) type User struct { Username string `json:username` Uuid string `json:uuid` SubSlugs []string `json:subs` + IsAdmin bool `json:admin` } func NewUser(un string) *User { @@ -21,6 +23,7 @@ func NewUser(un string) *User { return u } +/* func (u *User) UpdateFeed() error { for _, slug := range u.SubSlugs { pts := strings.Split(slug, ";") @@ -33,6 +36,7 @@ func (u *User) UpdateFeed() error { } return nil } +*/ func (m *model) SaveUser(u *User) error { var err error @@ -47,6 +51,13 @@ func (m *model) SaveUser(u *User) error { if err = m.bolt.SetValue(bkt, "username", u.Username); err != nil { return err } + var adminVal = "false" + if u.IsAdmin { + adminVal = "true" + } + if err = m.bolt.SetValue(bkt, "admin", adminVal); err != nil { + return err + } var newSubs []string for _, v := range u.SubSlugs { if strings.TrimSpace(v) != "" { @@ -60,7 +71,39 @@ func (m *model) SaveUser(u *User) error { return nil } +func (m *model) isValidUser(uid string) bool { + var err error + if err = m.bolt.OpenDB(); err != nil { + return false + } + defer m.bolt.CloseDB() + ret := new(User) + bkt := []string{"users", uid} + ret.Uuid = uid + if ret.Username, err = m.bolt.GetValue(bkt, "username"); err != nil { + fmt.Println("Error getting username value:", err.Error()) + return false + } + return true +} + +func (m *model) GetUserByEmail(email string) (*User, error) { + if email == "" { + return nil, errors.New("No email given") + } + u := m.GetAllUsers() + for i := range u { + if u[i].Username == email { + return &u[i], nil + } + } + return nil, errors.New("No user found") +} + func (m *model) GetUser(uid string) (*User, error) { + if uid == "" { + return nil, errors.New("No user id given") + } var err error if err = m.bolt.OpenDB(); err != nil { return nil, err @@ -73,6 +116,8 @@ func (m *model) GetUser(uid string) (*User, error) { fmt.Println("Error getting username value:", err.Error()) return nil, err } + admin, _ := m.bolt.GetValue(bkt, "admin") + ret.IsAdmin = (admin == "true") var subs string if subs, err = m.bolt.GetValue(bkt, "subs"); err != nil { return nil, err @@ -143,3 +188,50 @@ func (m *model) GetUserIdList() []string { ret, _ = m.bolt.GetBucketList(bkt) return ret } + +// updateUserPassword +// Takes a user id and a password +// Fails if the user doesn't exist +func (m *model) updateUserPassword(uid, password string) error { + cryptPw, cryptError := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if cryptError != nil { + return cryptError + } + if _, err := m.GetUser(uid); err != nil { + return err + } + + if err := m.bolt.OpenDB(); err != nil { + return err + } + defer m.bolt.CloseDB() + + usrPath := []string{"users", uid} + return m.bolt.SetValue(usrPath, "password", string(cryptPw)) +} + +// Is the uid and pw given valid? +func (m *model) checkCredentials(uid, pw string) error { + var err error + if err = m.bolt.OpenDB(); err != nil { + return err + } + defer m.bolt.CloseDB() + + var uPw string + usrPath := []string{"users", uid} + if uPw, err = m.bolt.GetValue(usrPath, "password"); err != nil { + return err + } + return bcrypt.CompareHashAndPassword([]byte(uPw), []byte(pw)) +} + +func (m *model) hasAdminUser() bool { + users := m.GetAllUsers() + for i := range users { + if users[i].IsAdmin { + return true + } + } + return false +} diff --git a/page_session.go b/page_session.go index a5fb7cd..ebc40a3 100644 --- a/page_session.go +++ b/page_session.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "net/http" @@ -16,6 +17,9 @@ type pageSession struct { } func (p *pageSession) getStringValue(key string) (string, error) { + if p.session == nil { + return "", errors.New("Session is nil") + } val := p.session.Values[key] var retVal string var ok bool diff --git a/helper_dilbert.go b/plugin_src/plugin_dilbert.go similarity index 56% rename from helper_dilbert.go rename to plugin_src/plugin_dilbert.go index d1a8838..1aba78d 100644 --- a/helper_dilbert.go +++ b/plugin_src/plugin_dilbert.go @@ -1,5 +1,8 @@ package main +import ( + "C" +) import ( "errors" "fmt" @@ -9,33 +12,33 @@ import ( "github.com/PuerkitoBio/goquery" ) -func downloadDilbertList() []Comic { - var ret []Comic - ret = append(ret, *NewComic("dilbert", "Dilbert", "Scott Adams", "dilbert")) +func getFeedList() []Feed { + var ret []Feed + ret = append(ret, *NewFeed("dilbert", "Dilbert", "Scott Adams", "dilbert")) return ret } -func getDilbertRssItem(slug string) (string, error) { - desc, err := getDilbertFeedDesc(time.Now()) +func getRssItem(slug string) (string, error) { + desc, err := getFeedDesc(time.Now()) if err != nil { return "", err } - comic, err := m.GetComic(SRC_DILBERT, slug) + feed, err := m.GetFeed(SRC_DILBERT, slug) if err != nil { return "", err } desc = "" ret := " \n" - ret += " " + comic.Name + "\n" - ret += " " + comic.LastUpdate.Format(time.RFC1123Z) + "\n" - ret += " dilbert;" + slug + ";" + comic.LastUpdate.Format(time.RFC1123Z) + "\n" - ret += " " + getDilbertComicUrl(time.Now()) + "\n" + ret += " " + feed.Name + "\n" + ret += " " + feed.LastUpdate.Format(time.RFC1123Z) + "\n" + ret += " dilbert;" + slug + ";" + feed.LastUpdate.Format(time.RFC1123Z) + "\n" + ret += " " + getFeedUrl(time.Now()) + "\n" ret += " " + desc + "\n" ret += " \n" return ret, nil } -func getDilbertComicUrl(date time.Time) string { +func getFeedUrl(date time.Time) string { return fmt.Sprintf( "http://dilbert.com/strip/%4d-%02d-%02d", date.Year(), @@ -44,8 +47,8 @@ func getDilbertComicUrl(date time.Time) string { ) } -func getDilbertFeedDesc(date time.Time) (string, error) { - res, err := http.Get(getDilbertComicUrl(date)) +func getFeedDesc(date time.Time) (string, error) { + res, err := http.Get(getFeedUrl(date)) if err != nil { return "", err } diff --git a/helper_gocomics.go b/plugin_src/plugin_gocomics.go similarity index 85% rename from helper_gocomics.go rename to plugin_src/plugin_gocomics.go index 9f5fcb7..5675e42 100644 --- a/helper_gocomics.go +++ b/plugin_src/plugin_gocomics.go @@ -1,6 +1,7 @@ package main import ( + "C" "errors" "fmt" "net/http" @@ -10,8 +11,8 @@ import ( "github.com/PuerkitoBio/goquery" ) -func downloadGoComicsList() []Comic { - var ret []Comic +func downloadList() { + var ret []Feed lstUrl := "http://www.gocomics.com/comics/a-to-z" res, err := http.Get(lstUrl) if err != nil { @@ -47,14 +48,14 @@ func downloadGoComicsList() []Comic { } author = strings.TrimPrefix(author, "By ") author = strings.Replace(author, "\u0026", "&", -1) - ret = append(ret, *NewComic(slug, name, author, "gocomics")) + ret = append(ret, *NewFeed(slug, name, author, "gocomics")) } }) return ret } -func getGoComicsRssItem(slug string) (string, error) { - desc, err := getGoComicsFeedDesc(slug, time.Now()) +func getRssItem(slug string) (string, error) { + desc, err := getFeedDesc(slug, time.Now()) if err != nil { return "", err } @@ -73,7 +74,7 @@ func getGoComicsRssItem(slug string) (string, error) { return ret, nil } -func getGoComicsComicUrl(slug string, date time.Time) string { +func getFeedUrl(slug string, date time.Time) string { return fmt.Sprintf( "http://www.gocomics.com/%s/%04d/%02d/%02d", slug, @@ -83,8 +84,8 @@ func getGoComicsComicUrl(slug string, date time.Time) string { ) } -func getGoComicsFeedDesc(slug string, date time.Time) (string, error) { - res, err := http.Get(getGoComicsComicUrl(slug, date)) +func getFeedDesc(slug string, date time.Time) (string, error) { + res, err := http.Get(getFeedUrl(slug, date)) if err != nil { return "", err } diff --git a/plugin_src/plugin_xkcd.go b/plugin_src/plugin_xkcd.go new file mode 100644 index 0000000..d1f40cb --- /dev/null +++ b/plugin_src/plugin_xkcd.go @@ -0,0 +1,84 @@ +package main + +import ( + "C" + "errors" + "fmt" + "net/http" + "time" + + "github.com/PuerkitoBio/goquery" +) + +func getFeedList() []Feed { + var ret []Feed + ret = append(ret, *NewFeed("xkcd", "XKCD", "Randall Munroe", "xkcd")) + return ret +} + +func getRssItem(slug string) (string, error) { + desc, err := getFeedDesc(time.Now()) + if err != nil { + return "", err + } + feed, err := m.GetFeed(SRC_XKCD, slug) + if err != nil { + return "", err + } + desc = "" + ret := " \n" + ret += " " + feed.Name + "\n" + ret += " " + feed.LastUpdate.Format(time.RFC1123Z) + "\n" + ret += " xkcd;" + slug + ";" + feed.LastUpdate.Format(time.RFC1123Z) + "\n" + ret += " " + getUrl(time.Now()) + "\n" + ret += " " + desc + "\n" + ret += " \n" + return ret, nil +} + +func getFeedUrl(date time.Time) (string, error) { + var isComicDay = func(dt time.Time) bool { + return dt.Weekday() == time.Monday || dt.Weekday() == time.Wednesday || dt.Weekday() == time.Friday + } + if !isComicDay(dt) { + return "", errors.New("No URL for the given day") + } + var num int + wrkDate := time.Date(2005, time.August, 19, 0, 0, 0, 0, time.UTC) + for wrkDate.Before(dt) { + if isComicDay(wrkDate) { + num++ + } + wrkDate = wrkDate.Add(time.Hour * 24) + } + return fmt.Sprintf("https://xkcd.com/%d", num), nil +} + +func getFeedDesc(date time.Time) (string, error) { + res, err := http.Get(getUrl(date)) + if err != nil { + return "", err + } + defer res.Body.Close() + if res.StatusCode != 200 { + return "", errors.New(fmt.Sprintf("Status code error: %d %s", res.StatusCode, res.Status)) + } + + // Load the HTML document + doc, err := goquery.NewDocumentFromReader(res.Body) + if err != nil { + return "", err + } + // Find the Picture + sel := doc.Find("div#comic>img") + src, exists := sel.Attr("src") + if !exists { + return "", errors.New("Couldn't find image source") + } + src = "https:" + src + title, exists := sel.Attr("title") + if !exists { + title = "" + } + return "

" + title + "

", nil +} diff --git a/templates/footer.html b/templates/footer.html new file mode 100644 index 0000000..3076371 --- /dev/null +++ b/templates/footer.html @@ -0,0 +1,11 @@ + + diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..608556f --- /dev/null +++ b/templates/header.html @@ -0,0 +1,9 @@ +
+ {{.FlashMessage}} +
+
+ {{ if .SubTitle }} +
+

{{.SubTitle}}

+
+ {{ end }} diff --git a/templates/htmlfooter.html b/templates/htmlfooter.html new file mode 100644 index 0000000..10480dd --- /dev/null +++ b/templates/htmlfooter.html @@ -0,0 +1,6 @@ +
+ {{ range $i, $v := .Scripts }} + + {{ end }} + + diff --git a/templates/htmlheader.html b/templates/htmlheader.html new file mode 100644 index 0000000..6fb41eb --- /dev/null +++ b/templates/htmlheader.html @@ -0,0 +1,25 @@ + + + + + + + + + + + {{.Site.Title}} - {{.SubTitle}} + + + + {{ range $i, $v := .Stylesheets }} + + {{ end }} + {{ range $i, $v := .HeaderScripts }} + + {{ end }} + + +
diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..725b0e7 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,22 @@ +
+
+
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+
diff --git a/templates/user_dashboard.html b/templates/user_dashboard.html new file mode 100644 index 0000000..e69de29