Initial Commit

This commit is contained in:
Brian Buller 2018-01-27 11:58:40 -06:00
commit 6bf84d8b54
14 changed files with 1756 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Ignore the binary
songbook

54
admin.go Normal file
View File

@ -0,0 +1,54 @@
package main
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
)
func handleAdmin(w http.ResponseWriter, req *http.Request) {
page := initPageData(w, req)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
vars := mux.Vars(req)
if !page.LoggedIn {
page.show("admin-login.html", w)
} else {
action := vars["action"]
switch action {
case "dologin":
handleAdminDoLogin(w, req)
case "dologout":
handleAdminDoLogout(w, req)
case "build":
default:
loadAndShowSongs(w, req, page)
}
}
}
func handleAdminDoLogin(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
username := req.FormValue("un")
password := req.FormValue("pw")
var success bool
if strings.TrimSpace(username) != "" && strings.TrimSpace(password) != "" {
}
page.session.setFlashMessage("Login Successful!", "success")
redirect("/admin", w, req)
}
func handleAdminDoLogout(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, "Admin Logout")
}
func loadAndShowSongs(w http.ResponseWriter, req *http.Request, page *pageData) {
}
func buildSongbook(w http.ResponseWriter, req *http.Request, page *pageData) {
}

1226
assets.go Normal file

File diff suppressed because it is too large Load Diff

52
helpers.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"fmt"
"html/template"
"net/http"
)
type pageData struct {
Title string
SubTitle string
Stylesheets []string
HeaderScripts []string
Scripts []string
FlashMessage string
FlashClass string
LoggedIn bool
session *pageSession
Site *siteData
TemplateData interface{}
}
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
}
// outputTemplate
// Spit out a template
func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
n := "/templates/" + tmplName
l := template.Must(template.New("layout").Parse(FSMustString(site.DevMode, n)))
t := template.Must(l.Parse(FSMustString(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)
}

159
main.go Normal file
View File

@ -0,0 +1,159 @@
package main
//go:generate esc -o assets.go assets templates songs
import (
"bufio"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"golang.org/x/crypto/ssh/terminal"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/justinas/alice"
)
var port int
var ip string
var dir string
var sessionSecret = "AChKj7na128CrKPPG0Rc"
var sessionStore = sessions.NewCookieStore([]byte(sessionSecret))
var r *mux.Router
var site *siteData
var model *model
type siteData struct {
Title string
DevMode bool
}
func init() {
const (
portDefault = 8080
portUsage = "the port to listen on"
ipDefault = "127.0.0.1"
ipUsage = "the ip mask to listen for connections from"
dirDefault = "./"
dirUsage = "the directory to serve the app out of\nThere should be a 'songs' directory here."
)
flag.StringVar(&ip, "ip", ipDefault, ipUsage)
flag.IntVar(&port, "port", portDefault, portUsage)
flag.StringVar(&dir, "dir", dirDefault, dirUsage)
if !strings.HasSuffix(dir, "/") {
dir = dir + "/"
}
}
func main() {
flag.Parse()
loadModel()
r := mux.NewRouter()
r.StrictSlash(true)
//r.PathPrefix("/assets/").Handler(http.FileServer(FS(site.DevMode)))
// Public Subrouter
pub := r.PathPrefix("/").Subrouter()
pub.HandleFunc("/", handleMain)
admin := r.PathPrefix("/admin").Subrouter()
admin.HandleFunc("/", handleAdmin)
admin.HandleFunc("/dologin", handleAdminDoLogin)
admin.HandleFunc("/dologout", handleAdminDoLogout)
admin.HandleFunc("/{action}", handleAdmin)
http.Handle("/", r)
chain := alice.New(loggingHandler).Then(r)
listenAt := fmt.Sprintf("%s:%d", ip, port)
fmt.Println("Songbook Server listening at", listenAt)
log.Fatal(http.ListenAndServe(listenAt, chain))
}
func loadModel() {
model := NewModel(dir)
site = new(siteData)
if !model.hasUser() {
reader := bufio.NewReader(os.Stdin)
fmt.Println("Create new Admin user")
fmt.Print("Username: ")
un, _ := reader.ReadString('\n')
un = strings.TrimSpace(un)
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!")
}
}
}
}
func handleMain(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
output, err := ioutil.ReadFile(dir + "currentsongs.html")
if err != nil {
output = []byte("Error reading current songs")
}
fmt.Fprint(w, string(output))
}
func initPageData(w http.ResponseWriter, req *http.Request) *pageData {
if site.DevMode {
w.Header().Set("Cache-Control", "no-cache")
}
p := new(pageData)
// Get the session
var err error
var s *sessions.Session
if s, err = sessionStore.Get(req, "songbook"); err != nil {
http.Error(w, err.Error(), 500)
return p
}
p.session = new(pageSession)
p.session.session = s
p.session.req = req
p.session.w = w
// First check if we're logged in
userName, _ := p.session.getStringValue("name")
_ = userName
p.Site = site
p.SubTitle = "Songbook"
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/vendor/font-awesome/css/font-awesome.min.css")
p.Stylesheets = append(p.Stylesheets, "/assets/css/gjvote.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"
}
return p
}
func loggingHandler(h http.Handler) http.Handler {
return handlers.LoggingHandler(os.Stdout, h)
}

61
model.go Normal file
View File

@ -0,0 +1,61 @@
package main
import (
"bytes"
"io/ioutil"
"strings"
"github.com/BurntSushi/toml"
)
func NewSongbookConfig(dir string) {
sc := &SongbookConfig{ConfigPath: dir + "songbook.toml"}
err := sc.Load()
return sc, err
}
type SongbookConfig struct {
Username string `toml:"username"`
Password string `toml:"enc_pw"`
ConfigPath string `toml:"-"`
}
func (sc *SongbookConfig) Load() error {
tomlData, err := ioutil.ReadFile(sc.ConfigPath)
if err != nil {
// Couldn't find the file, save a new one
if err = sc.Save(); err != nil {
return err
}
}
if _, err := toml.Decode(string(tomlData), &sc); err != nil {
return err
}
return nil
}
func (sc *SongbookConfig) Save() error {
buf := new(bytes.Buffer)
if err := toml.NewEncoder(buf).Encode(sc); err != nil {
return err
}
return ioutil.WriteFile(sc.ConfigPath, buf.Bytes(), 0644)
}
func NewModel(dir string) *model {
m := new(model)
m.config = NewSongbookConfig(dir)
return m
}
type model struct {
config *SongbookConfig
}
func (m *model) hasUser() bool {
// is there a user in the config?
if strings.TrimSpace(m.config.Username) == "" || strings.TrimSpace(m.Config.Password) == "" {
return false
}
return true
}

54
page_session.go Normal file
View File

@ -0,0 +1,54 @@
package main
import (
"fmt"
"net/http"
"github.com/gorilla/sessions"
)
// This is basically a convenience struct for
// easier session management (hopefully ;)
type pageSession struct {
session *sessions.Session
req *http.Request
w http.ResponseWriter
}
func (p *pageSession) getStringValue(key string) (string, error) {
val := p.session.Values[key]
var retVal string
var ok bool
if retVal, ok = val.(string); !ok {
return "", fmt.Errorf("Unable to create string from %s", key)
}
return retVal, nil
}
func (p *pageSession) setStringValue(key, val string) {
p.session.Values[key] = val
p.session.Save(p.req, p.w)
}
func (p *pageSession) setFlashMessage(msg, status string) {
p.setStringValue("flash_message", msg)
p.setStringValue("flash_status", status)
}
func (p *pageSession) getFlashMessage() (string, string) {
var err error
var msg, status string
if msg, err = p.getStringValue("flash_message"); err != nil {
return "", "hidden"
}
if status, err = p.getStringValue("flash_status"); err != nil {
return "", "hidden"
}
p.setFlashMessage("", "hidden")
return msg, status
}
func (p *pageSession) expireSession() {
p.session.Options.MaxAge = -1
p.session.Save(p.req, p.w)
}

BIN
songbook Executable file

Binary file not shown.

View File

@ -0,0 +1,23 @@
<h1>Admin Login</h1>
<div class="center">
<form class="pure-form pure-form-aligned" action="/admin/dologin" method="POST">
<fieldset>
<div class="pure-control-group">
<label for="un">Username</label>
<input id="un" name="un" type="text" placeholder="username" autofocus>
</div>
<div class="pure-control-group">
<label for="pw">Password</label>
<input id="pw" name="pw" type="password" placeholder="password">
</div>
<div class="pure-controls">
<label for="remember" class="pure-checkbox">
<input id="remember" name="remember" type="checkbox"> Remember Me
</label>
</div>
<button type="submit" class="pure-button pure-button-primary">Submit</button>
</fieldset>
</form>
</div>

11
templates/footer.html Normal file
View File

@ -0,0 +1,11 @@
</div>
<div id="modal-overlay">
<div>
<h1 id="modal-title"></h1>
<h2 id="modal-subtitle"></h2>
<div id="modal-body"></div>
<div id="modal-buttons">
</div>
<div class="reset-pull"></div>
</div>
</div>

9
templates/header.html Normal file
View File

@ -0,0 +1,9 @@
<div class="flash center {{.FlashClass}}">
{{.FlashMessage}}
</div>
<div class="content">
{{ if .SubTitle }}
<div class="header-menu">
<h2>{{.SubTitle}}</h2>
</div>
{{ end }}

View File

@ -0,0 +1,6 @@
</div>
{{ range $i, $v := .Scripts }}
<script src="{{ $v }}"></script>
{{ end }}
</body>
</html>

25
templates/htmlheader.html Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta http-equiv="Cache-control" content="No-Cache">
<link rel="apple-touch-icon" href="/assets/img/favicon.png" type="image/png">
<link rel="shortcut icon" href="/assets/img/favicon.png" type="image/png">
<title>{{.Site.Title}} - {{.SubTitle}}</title>
<!--[if lt IE 9]>
<script src="http://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7/html5shiv.js"></script>
<![endif]-->
{{ range $i, $v := .Stylesheets }}
<link rel="stylesheet" href="{{ $v }}">
{{ end }}
{{ range $i, $v := .HeaderScripts }}
<script src="{{ $v }}"></script>
{{ end }}
</head>
<body>
<div id="layout">

View File

@ -0,0 +1,74 @@
{{ if not .TemplateData.Songs }}
<div>No songs are available</div>
{{ else }}
<form class="pure-form" action="/admin/build" method="POST">
<fieldset>
<input id="chosen_songs" name="chosen_songs" value="" type="hidden">
<button class="pure-button pure-button-primary" type="submit">Build File</button>
</fieldset>
</form>
<div>
<label for="filter">Filter</label>
<input onkeyup="updateFilter(this.value);" name="filter" value="" placeholder="Filter Songs">
<table id="song-table" class="pure-table pure-table-bordered center">
<thead>
<tr>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
{{ range $i, $v := .TemplateData.Songs }}
<tr data-file="{{$v.Name}}>
<td>{{$v.Name}}</td>
<td>
<a class="pure-button pure-button-secondary" onclick="javascript:toggleFile(this, '{{$v.Name}});">Add</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<script>
var selectedFiles = [];
var tablerows = document.getElementsByTagName("tr");
function updateFilter(flt) {
flt = flt.toLowerCase();
for(var i = 0; i < tablerows.length; i++) {
if(tablerows[i].dataset.file.toLowerCase().startsWith(flt)) {
tablerows[i].classList.remove("hidden");
} else {
tablerows[i].classList.add("hidden");
}
}
}
function toggleFile(btn, sng) {
if(!btn.classList.contains("pure-button-warning")) {
btn.classList.remove("pure-button-secondary");
btn.classList.add("pure-button-warning");
for(var i = 0; i < selectedFiles.length; i++) {
if(selectedFiles[i] == sng) { return; }
}
selectedFiles.push(sng);
btn.innerText = "Remove";
} else {
btn.classList.remove("pure-button-warning");
btn.classList.add("pure-button-secondary");
for(var i = 0; i < selectedFiles.length; i++) {
if(selectedFiles[i] == sng) { delete selectedFiles[i]; }
}
btn.innerText = "Add";
}
updateFilesInput();
}
function removeSong(sng) {
for(var i = 0; i < selectedFiles.length; i++ {
if(selectedFiles[i] == sng) { delete selectedFiles[i]; }
}
updateFilesInput();
}
</script>
{{ end }}