Initial Commit
This commit is contained in:
commit
6bf84d8b54
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Ignore the binary
|
||||
songbook
|
54
admin.go
Normal file
54
admin.go
Normal 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) {
|
||||
|
||||
}
|
52
helpers.go
Normal file
52
helpers.go
Normal 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
159
main.go
Normal 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
61
model.go
Normal 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
54
page_session.go
Normal 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)
|
||||
}
|
23
templates/admin-login.html
Normal file
23
templates/admin-login.html
Normal 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
11
templates/footer.html
Normal 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
9
templates/header.html
Normal 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 }}
|
6
templates/htmlfooter.html
Normal file
6
templates/htmlfooter.html
Normal file
@ -0,0 +1,6 @@
|
||||
</div>
|
||||
{{ range $i, $v := .Scripts }}
|
||||
<script src="{{ $v }}"></script>
|
||||
{{ end }}
|
||||
</body>
|
||||
</html>
|
25
templates/htmlheader.html
Normal file
25
templates/htmlheader.html
Normal 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">
|
74
templates/song-picker.html
Normal file
74
templates/song-picker.html
Normal 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 }}
|
Loading…
Reference in New Issue
Block a user