package main //go:generate esc -o assets.go assets templates import ( "bufio" "fmt" "html/template" "log" "net/http" "os" "os/signal" "strconv" "strings" "syscall" "time" "golang.org/x/crypto/ssh/terminal" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/justinas/alice" ) // AppName Application name const AppName = "gjvote" // DbName Database name, which is .db const DbName = AppName + ".db" // pageData is stuff that changes per request type pageData struct { Site *siteData Title string HideTitleImage bool SubTitle string Stylesheets []string HeaderScripts []string Scripts []string FlashMessage string FlashClass string LoggedIn bool Menu []menuItem BottomMenu []menuItem HideAdminMenu bool session *pageSession CurrentJam string ClientId string ClientIsAuth bool ClientIsServer bool AuthMode int PublicMode int TemplateData interface{} } type menuItem struct { Label string Location string Icon string } var sessionStore *sessions.CookieStore var r *mux.Router var m *model func main() { var err error if m, err = NewModel(); err != nil { errorExit("Unable to initialize Model: " + err.Error()) } loadConfig() if err = m.site.SaveToDB(); err != nil { errorExit("Unable to save site config to DB: " + err.Error()) } // Save changes to the DB every 5 minutes go func() { for { m.saveChanges() time.Sleep(5 * time.Minute) } }() initialize() // We should have a session secret by now, initialize the store sessionStore = sessions.NewCookieStore([]byte(m.site.sessionSecret)) r = mux.NewRouter() r.StrictSlash(true) if m.site.DevMode { fmt.Println("Operating in Development Mode") } r.PathPrefix("/assets/").Handler(http.FileServer(FS(m.site.DevMode))) // Public Subrouter pub := r.PathPrefix("/").Subrouter() pub.HandleFunc("/", handleMain) pub.HandleFunc("/vote", handlePublicSaveVote) pub.HandleFunc("/image/{teamid}/{imageid}", handleImageRequest) pub.HandleFunc("/thumbnail/{teamid}/{imageid}", handleThumbnailRequest) pub.HandleFunc("/team/{id}", handleTeamMgmtRequest) pub.HandleFunc("/team/{id}/{function}", handleTeamMgmtRequest) pub.HandleFunc("/team/{id}/{function}/{subid}", handleTeamMgmtRequest) // API Subrouter //api := r.PathPrefix("/api").Subtrouter() // Admin Subrouter admin := r.PathPrefix("/admin").Subrouter() admin.HandleFunc("/", handleAdmin) admin.HandleFunc("/dologin", handleAdminDoLogin) admin.HandleFunc("/dologout", handleAdminDoLogout) admin.HandleFunc("/{category}", handleAdmin) admin.HandleFunc("/{category}/{id}", handleAdmin) admin.HandleFunc("/{category}/{id}/{function}", handleAdmin) admin.HandleFunc("/{category}/{id}/{function}/{subid}", handleAdmin) http.Handle("/", r) chain := alice.New(loggingHandler).Then(r) // Set up a channel to intercept Ctrl+C for graceful shutdowns c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c // Save the changes when the app quits fmt.Println("\nFinishing up...") m.saveChanges() os.Exit(0) }() fmt.Printf("Listening on port %d\n", m.site.Port) log.Fatal(http.ListenAndServe(m.site.Ip+":"+strconv.Itoa(m.site.Port), chain)) } func loadConfig() { if len(os.Args) > 1 { for _, v := range os.Args { key := v val := "" eqInd := strings.Index(v, "=") if eqInd > 0 { // It's a key/val argument key = v[:eqInd] val = v[eqInd+1:] } switch key { case "-title": m.site.Title = val fmt.Print("Set site title: ", m.site.Title, "\n") case "-ip": m.site.Ip = val fmt.Print("Set site IP: ", m.site.Ip, "\n") case "-port": var tryPort int var err error if tryPort, err = strconv.Atoi(val); err != nil { fmt.Print("Invalid port given: ", val, " (Must be an integer)\n") tryPort = m.site.Port } // TODO: Make sure a valid port number is given m.site.Port = tryPort case "-session-name": m.site.SessionName = val case "-server-dir": // TODO: Probably check if the given directory is valid m.site.ServerDir = val case "-help", "-h", "-?": printHelp() done() case "-dev": m.site.DevMode = true case "-reset-defaults": resetToDefaults() done() } } } } func initialize() { // Test if we have an admin user first if !m.hasUser() { // 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) 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!") } } assertError(m.updateUserPassword(email, string(pw1))) } // Now test if the 'current jam' is named if m.jam.Name == "" { reader := bufio.NewReader(os.Stdin) fmt.Println("Create New Game Jam") fmt.Print("GameJam Name: ") gjName, _ := reader.ReadString('\n') gjName = strings.TrimSpace(gjName) m.jam.Name = gjName assertError(m.jam.SaveToDB()) } if m.jam.Name != "" { fmt.Println("Current Jam Name: " + m.jam.Name) } else { fmt.Println("No Jam Name Specified") } 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()) } } 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.SessionName); 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 userEmail, _ := p.session.getStringValue("email") // With a valid account p.LoggedIn = m.isValidUserEmail(userEmail) p.Site = m.site p.SubTitle = "GameJam Voting" 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.HeaderScripts = append(p.HeaderScripts, "/assets/vendor/js/snack-min.js") p.Scripts = make([]string, 0, 0) p.Scripts = append(p.Scripts, "/assets/js/gjvote.js") p.FlashMessage, p.FlashClass = p.session.getFlashMessage() if p.FlashClass == "" { p.FlashClass = "hidden" } // Build the menu if p.LoggedIn { p.Menu = append(p.Menu, menuItem{"Admin", "/admin", "fa-key"}) p.Menu = append(p.Menu, menuItem{"Teams", "/admin/teams", "fa-users"}) p.Menu = append(p.Menu, menuItem{"Games", "/admin/games", "fa-gamepad"}) p.Menu = append(p.Menu, menuItem{"Votes", "/admin/votes", "fa-sticky-note"}) p.Menu = append(p.Menu, menuItem{"Archive", "/admin/archive", "fa-archive"}) p.Menu = append(p.Menu, menuItem{"Clients", "/admin/clients", "fa-desktop"}) p.BottomMenu = append(p.BottomMenu, menuItem{"Users", "/admin/users", "fa-user"}) p.BottomMenu = append(p.BottomMenu, menuItem{"Logout", "/admin/dologout", "fa-sign-out"}) } else { p.BottomMenu = append(p.BottomMenu, menuItem{"Admin", "/admin", "fa-sign-in"}) } p.HideAdminMenu = true p.ClientId = p.session.getClientId() var cl *Client if cl, err = m.GetClient(p.ClientId); err != nil { // A new client cl = NewClient(p.ClientId) } p.ClientIsAuth = cl.Auth p.ClientIsServer = clientIsServer(req) // Public Mode p.PublicMode = m.site.GetPublicMode() // Authentication Mode p.AuthMode = m.site.GetAuthMode() 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 } // 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(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 resetToDefaults() { def := NewSiteData(m) fmt.Println("Reset settings to defaults?") fmt.Print(m.site.Title, " -> ", def.Title, "\n") fmt.Print(m.site.Port, " -> ", def.Port, "\n") fmt.Print(m.site.SessionName, " -> ", def.SessionName, "\n") fmt.Print(m.site.ServerDir, " -> ", def.ServerDir, "\n") fmt.Println("Are you sure? (y/N): ") reader := bufio.NewReader(os.Stdin) conf, _ := reader.ReadString('\n') conf = strings.ToUpper(strings.TrimSpace(conf)) if strings.HasPrefix(conf, "Y") { if err := def.SaveToDB(); err != nil { errorExit("Error resetting to defaults: " + err.Error()) } fmt.Println("Reset to defaults") } } func printHelp() { help := []string{ "Game Jam Voting Help", " -help, -h, -? Print this message", " -dev Development mode, load assets from file system", " -port= Set the site port", " -session-name= Set the name of the session to be used", " -server-dir= Set the server directory", " This designates where the database will be saved", " and where the app will look for files if you're", " operating in 'development' mode (-dev)", " -title= Set the site title", " -current-jam=<name> Change the name of the current jam", " -reset-defaults Reset all configuration options to defaults", "", } for _, v := range help { fmt.Println(v) } } func done() { os.Exit(0) } func errorExit(msg string) { fmt.Println(msg) os.Exit(1) } func assertError(err error) { if err != nil { panic(err) } }