From 9a77a253422bd0ea09b86303b400f4e83ed7715c Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 8 Jun 2017 12:20:43 -0500 Subject: [PATCH] Making progress --- admin_clients.go | 57 +++++++++++ admin_endpoints.go | 47 +++++++++ admin_games.go | 6 ++ admin_teams.go | 70 +++++++++++++ adminEndpoints.go => admin_users.go | 114 +++------------------- assets/js/gjvote.js | 6 +- main.go | 16 +++ model.go | 53 +++------- model_clients.go | 101 +++++++++++++++++++ model_teams.go | 72 +++++--------- pageSession.go => page_session.go | 12 +++ publicEndpoints.go => public_endpoints.go | 6 +- templates/admin-activateclient.html | 22 +++++ templates/admin-addteam.html | 2 +- templates/admin-adduser.html | 2 +- templates/admin-clients.html | 28 ++++++ templates/admin-editteam.html | 2 +- templates/admin-login.html | 2 +- templates/admin-menu.html | 7 +- templates/admin-teams.html | 4 +- templates/header.html | 5 +- 21 files changed, 440 insertions(+), 194 deletions(-) create mode 100644 admin_clients.go create mode 100644 admin_endpoints.go create mode 100644 admin_games.go create mode 100644 admin_teams.go rename adminEndpoints.go => admin_users.go (50%) create mode 100644 model_clients.go rename pageSession.go => page_session.go (80%) rename publicEndpoints.go => public_endpoints.go (88%) create mode 100644 templates/admin-activateclient.html create mode 100644 templates/admin-clients.html diff --git a/admin_clients.go b/admin_clients.go new file mode 100644 index 0000000..19f7504 --- /dev/null +++ b/admin_clients.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "net" + "net/http" + + "github.com/gorilla/mux" +) + +func handleAdminClients(w http.ResponseWriter, req *http.Request, page *pageData) { + vars := mux.Vars(req) + page.SubTitle = "Clients" + clientId := vars["id"] + clientIp, _, _ := net.SplitHostPort(req.RemoteAddr) + if clientId == "" { + type clientsPageData struct { + Clients []Client + } + page.TemplateData = clientsPageData{Clients: dbGetAllClients()} + page.SubTitle = "Clients" + page.show("admin-clients.html", w) + } else { + switch vars["function"] { + case "add": + type actClientPageData struct { + Id string + } + page.TemplateData = actClientPageData{Id: clientId} + page.show("admin-activateclient.html", w) + case "auth": + email := req.FormValue("email") + password := req.FormValue("password") + remember := req.FormValue("remember") + if doLogin(email, password) == nil { + // Received a valid login + // Authenticate the client + if dbAuthClient(clientId, clientIp) == nil { + page.session.setFlashMessage("Client Authenticated", "success") + } else { + page.session.setFlashMessage("Client Authentication Failed", "error") + } + if remember == "on" { + // Go ahead and log in + page.session.setStringValue("email", email) + redirect("/admin/clients", w, req) + } + } + redirect("/", w, req) + case "deauth": + remember := req.FormValue("remember") + fmt.Println("Remember: ", remember) + dbDeAuthClient(clientId) + redirect("/admin/clients", w, req) + } + } +} diff --git a/admin_endpoints.go b/admin_endpoints.go new file mode 100644 index 0000000..7e87b39 --- /dev/null +++ b/admin_endpoints.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +func initAdminRequest(w http.ResponseWriter, req *http.Request) *pageData { + p := InitPageData(w, req) + p.Stylesheets = append(p.Stylesheets, "/assets/css/admin.css") + p.Scripts = append(p.Scripts, "/assets/js/admin.js") + p.HideAdminMenu = false + + return p +} + +// Main admin handler, routes the request based on the category +func handleAdmin(w http.ResponseWriter, req *http.Request) { + page := initAdminRequest(w, req) + vars := mux.Vars(req) + if !page.LoggedIn { + if vars["category"] == "clients" && + vars["id"] != "" && + (vars["function"] == "add" || vars["function"] == "auth") { + // When authenticating a client, we have an all-in-one login/auth page + handleAdminClients(w, req, page) + } else { + page.SubTitle = "Admin Login" + page.show("admin-login.html", w) + } + } else { + adminCategory := vars["category"] + switch adminCategory { + case "users": + handleAdminUsers(w, req, page) + case "teams": + handleAdminTeams(w, req, page) + case "games": + handleAdminGames(w, req, page) + case "clients": + handleAdminClients(w, req, page) + default: + page.show("admin-main.html", w) + } + } +} diff --git a/admin_games.go b/admin_games.go new file mode 100644 index 0000000..0beb47f --- /dev/null +++ b/admin_games.go @@ -0,0 +1,6 @@ +package main + +import "net/http" + +func handleAdminGames(w http.ResponseWriter, req *http.Request, page *pageData) { +} diff --git a/admin_teams.go b/admin_teams.go new file mode 100644 index 0000000..02be4fc --- /dev/null +++ b/admin_teams.go @@ -0,0 +1,70 @@ +package main + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData) { + vars := mux.Vars(req) + page.SubTitle = "Teams" + teamId := vars["id"] + if teamId == "new" { + switch vars["function"] { + case "save": + name := req.FormValue("teamname") + if dbIsValidTeam(name) { + // A team with that name already exists + page.session.setFlashMessage("A team with the name "+name+" already exists!", "error") + } else { + if err := dbCreateNewTeam(name); err != nil { + page.session.setFlashMessage(err.Error(), "error") + } else { + page.session.setFlashMessage("Team "+name+" created!", "success") + } + } + redirect("/admin/teams", w, req) + default: + page.SubTitle = "Add New Team" + page.show("admin-addteam.html", w) + } + } else if teamId != "" { + if dbIsValidTeam(teamId) { + switch vars["function"] { + case "save": + tm := new(Team) + tm.UUID = teamId + tm.Name = req.FormValue("teamname") + if err := dbUpdateTeam(teamId, tm); err != nil { + page.session.setFlashMessage("Error updating team: "+err.Error(), "error") + } else { + page.session.setFlashMessage("Team Updated!", "success") + } + redirect("/admin/teams", w, req) + case "delete": + var err error + if err = dbDeleteTeam(teamId); err != nil { + page.session.setFlashMessage("Error deleting team: "+err.Error(), "error") + } + redirect("/admin/teams", w, req) + default: + page.SubTitle = "Edit Team" + t := dbGetTeam(teamId) + page.TemplateData = t + page.show("admin-editteam.html", w) + } + } else { + page.session.setFlashMessage("Couldn't find the requested team, please try again.", "error") + redirect("/admin/teams", w, req) + } + } else { + type teamsPageData struct { + Teams []Team + } + + page.TemplateData = teamsPageData{Teams: dbGetAllTeams()} + page.SubTitle = "Teams" + page.show("admin-teams.html", w) + } +} diff --git a/adminEndpoints.go b/admin_users.go similarity index 50% rename from adminEndpoints.go rename to admin_users.go index 67f2638..713fe4f 100644 --- a/adminEndpoints.go +++ b/admin_users.go @@ -1,62 +1,39 @@ package main import ( + "errors" "net/http" + "strings" "github.com/gorilla/mux" ) -func initAdminRequest(w http.ResponseWriter, req *http.Request) *pageData { - p := InitPageData(w, req) - p.Stylesheets = append(p.Stylesheets, "/assets/css/admin.css") - p.Scripts = append(p.Scripts, "/assets/js/admin.js") - - return p -} - -// handleAdmin -// Main admin handler, routes the request based on the category -func handleAdmin(w http.ResponseWriter, req *http.Request) { - page := initAdminRequest(w, req) - if !page.LoggedIn { - page.SubTitle = "Admin Login" - page.show("admin-login.html", w) - } else { - vars := mux.Vars(req) - adminCategory := vars["category"] - switch adminCategory { - case "users": - handleAdminUsers(w, req, page) - case "teams": - handleAdminTeams(w, req, page) - case "games": - handleAdminGames(w, req, page) - default: - page.show("admin-main.html", w) - } - } -} - // handleAdminDoLogin // Verify the provided credentials, set up a cookie (if requested) // and redirect back to /admin +// TODO: Set up the cookie func handleAdminDoLogin(w http.ResponseWriter, req *http.Request) { page := initAdminRequest(w, req) // Fetch the login credentials email := req.FormValue("email") password := req.FormValue("password") - if email != "" && password != "" { - if err := dbCheckCredentials(email, password); err != nil { - page.session.setFlashMessage("Invalid Login", "error") - } else { - page.session.setStringValue("email", email) - } - } else { + if err := doLogin(email, password); err != nil { page.session.setFlashMessage("Invalid Login", "error") + } else { + page.session.setStringValue("email", email) } redirect("/admin", w, req) } +// doLogin attempts to log in with the given email/password +// If it can't, it returns an error +func doLogin(email, password string) error { + if strings.TrimSpace(email) != "" && strings.TrimSpace(password) != "" { + return dbCheckCredentials(email, password) + } + return errors.New("Invalid Credentials") +} + // handleAdminDoLogout // Expire the session func handleAdminDoLogout(w http.ResponseWriter, req *http.Request) { @@ -136,64 +113,3 @@ func handleAdminUsers(w http.ResponseWriter, req *http.Request, page *pageData) page.show("admin-users.html", w) } } - -// handleAdminTeams -func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData) { - vars := mux.Vars(req) - page.SubTitle = "Teams" - teamId := vars["id"] - if teamId == "new" { - switch vars["function"] { - case "save": - name := req.FormValue("teamname") - if dbIsValidTeam(name) { - // A team with that name already exists - page.session.setFlashMessage("A team with the name "+name+" already exists!", "error") - } else { - if err := dbCreateNewTeam(name); err != nil { - page.session.setFlashMessage(err.Error(), "error") - } else { - page.session.setFlashMessage("Team "+name+" created!", "success") - } - } - redirect("/admin/teams", w, req) - default: - page.SubTitle = "Add New Team" - page.show("admin-addteam.html", w) - } - } else if teamId != "" { - if dbIsValidTeam(teamId) { - switch vars["function"] { - case "save": - page.session.setFlashMessage("Not implemented yet...", "success") - redirect("/admin/teams", w, req) - case "delete": - var err error - if err = dbDeleteTeam(teamId); err != nil { - page.session.setFlashMessage("Error deleting team: "+err.Error(), "error") - } - redirect("/admin/teams", w, req) - default: - page.SubTitle = "Edit Team" - t := dbGetTeam(teamId) - page.TemplateData = t - page.show("admin-editteam.html", w) - } - } else { - page.session.setFlashMessage("Couldn't find the requested team, please try again.", "error") - redirect("/admin/teams", w, req) - } - } else { - type teamsPageData struct { - Teams []Team - } - - page.TemplateData = teamsPageData{Teams: dbGetAllTeams()} - page.SubTitle = "Teams" - page.show("admin-teams.html", w) - } -} - -// handleAdminGames -func handleAdminGames(w http.ResponseWriter, req *http.Request, page *pageData) { -} diff --git a/assets/js/gjvote.js b/assets/js/gjvote.js index d0591f3..2797602 100644 --- a/assets/js/gjvote.js +++ b/assets/js/gjvote.js @@ -1,5 +1,5 @@ -function showAdminPanel() { - +function toggleAdminPanel() { + document.querySelector('#menu').classList.toggle('hidden'); } @@ -12,6 +12,6 @@ document.onkeydown = function(evt) { isEscape = (evt.keyCode == 27); } if(isEscape) { - showAdminPanel(); + toggleAdminPanel(); } } diff --git a/main.go b/main.go index ef8279f..06bb629 100644 --- a/main.go +++ b/main.go @@ -48,8 +48,11 @@ type pageData struct { LoggedIn bool Menu []menuItem BottomMenu []menuItem + HideAdminMenu bool session *pageSession CurrentJam string + ClientID string + ClientIsAuth bool TemplateData interface{} } @@ -159,6 +162,13 @@ func initialize() { fmt.Println("Error saving Current Jam") } } + + jmNm, err := dbGetCurrentJam() + if err == nil { + fmt.Println("Current Jam Name: " + jmNm) + } else { + fmt.Println(err.Error()) + } } func loggingHandler(h http.Handler) http.Handler { @@ -215,13 +225,19 @@ func InitPageData(w http.ResponseWriter, req *http.Request) *pageData { 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 if p.CurrentJam, err = dbGetCurrentJam(); err != nil { p.FlashMessage = "Error Loading Current GameJam: " + err.Error() p.FlashClass = "error" } + p.ClientID = p.session.getClientID() + p.ClientIsAuth = dbClientIsAuth(p.ClientID) + return p } diff --git a/model.go b/model.go index a07a815..33bddab 100644 --- a/model.go +++ b/model.go @@ -1,6 +1,11 @@ package main -import "github.com/br0xen/boltease" +import ( + "errors" + "strings" + + "github.com/br0xen/boltease" +) var db *boltease.DB var dbOpened bool @@ -24,7 +29,7 @@ func initDatabase() error { return err } // Create the path to the bucket to store jam informations - if err := db.MkBucketPath([]string{"jams"}); err != nil { + if err := db.MkBucketPath([]string{"jam"}); err != nil { return err } // Create the path to the bucket to store site config data @@ -41,51 +46,25 @@ func dbSetCurrentJam(name string) error { } func dbHasCurrentJam() bool { - var nm string var err error - if nm, err = dbGetCurrentJam(); err != nil { + if _, err = dbGetCurrentJam(); err != nil { return false } - ret, err := dbIsValidJam(nm) - return ret && err != nil + return true } func dbGetCurrentJam() (string, error) { - if err := db.OpenDB(); err != nil { + var ret string + var err error + if err = db.OpenDB(); err != nil { return "", err } defer db.CloseDB() - return db.GetValue([]string{"site"}, "current-jam") -} + ret, err = db.GetValue([]string{"site"}, "current-jam") -func dbIsValidJam(name string) (bool, error) { - var err error - if err = db.OpenDB(); err != nil { - return false, err + if err == nil && strings.TrimSpace(ret) == "" { + return ret, errors.New("No Jam Name Specified") } - defer db.CloseDB() - - // Get all keys in the jams bucket - var keys []string - if keys, err = db.GetKeyList([]string{"jams", name}); err != nil { - return false, err - } - // All valid gamejams will have: - // "name" - // "teams" - for _, v := range []string{"name", "teams"} { - found := false - for j := range keys { - if keys[j] == v { - found = true - break - } - } - if !found { - // If we make it here, we didn't find a key we need - return false, nil - } - } - return true, nil + return ret, err } diff --git a/model_clients.go b/model_clients.go new file mode 100644 index 0000000..f12bf31 --- /dev/null +++ b/model_clients.go @@ -0,0 +1,101 @@ +package main + +type Client struct { + UUID string + Auth bool +} + +func dbGetAllClients() []Client { + var ret []Client + var err error + if err = db.OpenDB(); err != nil { + return ret + } + defer db.CloseDB() + + var clientUids []string + if clientUids, err = db.GetBucketList([]string{"clients"}); err != nil { + return ret + } + for _, v := range clientUids { + if cl := dbGetClient(v); cl != nil { + ret = append(ret, *cl) + } + } + return ret +} + +func dbGetClient(id string) *Client { + var err error + if err = db.OpenDB(); err != nil { + return nil + } + defer db.CloseDB() + + cl := new(Client) + cl.UUID = id + cl.Auth = dbClientIsAuth(id) + return cl +} + +func dbAddDeauthClient(cid, ip string) error { + var err error + if err = db.OpenDB(); err != nil { + return err + } + defer db.CloseDB() + + err = db.SetBool([]string{"clients", cid}, "auth", false) + if err != nil { + return err + } + return db.SetValue([]string{"clients", cid}, "ip", ip) +} + +func dbAuthClient(cid, ip string) error { + var err error + if err = db.OpenDB(); err != nil { + return err + } + defer db.CloseDB() + + err = db.SetBool([]string{"clients", cid}, "auth", false) + if err != nil { + return err + } + return db.SetValue([]string{"clients", cid}, "ip", ip) +} + +func dbDeAuthClient(cid string) error { + var err error + if err = db.OpenDB(); err != nil { + return err + } + defer db.CloseDB() + + return db.SetBool([]string{"clients", cid}, "auth", false) +} + +func dbClientIsAuth(cid string) bool { + var err error + if err = db.OpenDB(); err != nil { + return false + } + defer db.CloseDB() + + var isAuth bool + if isAuth, err = db.GetBool([]string{"clients", cid}, "auth"); err != nil { + return false + } + return isAuth +} + +func dbUpdateClientIP(cid, ip string) error { + var err error + if err = db.OpenDB(); err != nil { + return err + } + defer db.CloseDB() + + return db.SetValue([]string{"clients", cid}, "ip", ip) +} diff --git a/model_teams.go b/model_teams.go index d437c38..3f57bd9 100644 --- a/model_teams.go +++ b/model_teams.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - "github.com/pborman/uuid" ) @@ -28,57 +26,41 @@ func dbCreateNewTeam(nm string) error { } defer db.CloseDB() - var currJam string - if currJam, err = dbGetCurrentJam(); err != nil { - return err - } - // Generate a UUID uuid := uuid.New() - teamPath := []string{"jams", currJam, "teams", uuid} + teamPath := []string{"teams", uuid} if err := db.MkBucketPath(teamPath); err != nil { - fmt.Println("Error at 39: " + uuid) return err } if err := db.SetValue(teamPath, "name", nm); err != nil { - fmt.Println("Error at 43") return err } if err := db.MkBucketPath(append(teamPath, "members")); err != nil { - fmt.Println("Error at 47") return err } gamePath := append(teamPath, "game") if err := db.MkBucketPath(gamePath); err != nil { - fmt.Println("Error at 52") return err } if err := db.SetValue(append(gamePath), "name", ""); err != nil { - fmt.Println("Error at 56") return err } return db.MkBucketPath(append(gamePath, "screenshots")) } -func dbIsValidTeam(nm string) bool { +func dbIsValidTeam(id string) bool { var err error - var currJam string if err = db.OpenDB(); err != nil { return false } defer db.CloseDB() - if currJam, err = dbGetCurrentJam(); err != nil { - return false - } - teamPath := []string{"jams", currJam, "teams"} + teamPath := []string{"teams"} if teamUids, err := db.GetBucketList(teamPath); err == nil { for _, v := range teamUids { - if tstName, err := db.GetValue(append(teamPath, v), "name"); err == nil { - if tstName == nm { - return true - } + if v == id { + return true } } } @@ -88,21 +70,19 @@ func dbIsValidTeam(nm string) bool { func dbGetAllTeams() []Team { var ret []Team var err error - var currJam string if err = db.OpenDB(); err != nil { return ret } defer db.CloseDB() - if currJam, err = dbGetCurrentJam(); err != nil { + teamPath := []string{"teams"} + var teamUids []string + if teamUids, err = db.GetBucketList(teamPath); err != nil { return ret } - teamPath := []string{"jams", currJam, "teams"} - if teamUids, err := db.GetBucketList(teamPath); err != nil { - for _, v := range teamUids { - if tm := dbGetTeam(v); tm != nil { - ret = append(ret, *tm) - } + for _, v := range teamUids { + if tm := dbGetTeam(v); tm != nil { + ret = append(ret, *tm) } } return ret @@ -110,17 +90,14 @@ func dbGetAllTeams() []Team { func dbGetTeam(id string) *Team { var err error - var currJam string if err = db.OpenDB(); err != nil { return nil } defer db.CloseDB() - if currJam, err = dbGetCurrentJam(); err != nil { - return nil - } - teamPath := []string{"jams", currJam, "teams", id} + teamPath := []string{"teams", id} tm := new(Team) + tm.UUID = id if tm.Name, err = db.GetValue(teamPath, "name"); err != nil { return nil } @@ -129,16 +106,12 @@ func dbGetTeam(id string) *Team { func dbGetTeamByName(nm string) *Team { var err error - var currJam string if err = db.OpenDB(); err != nil { return nil } defer db.CloseDB() - if currJam, err = dbGetCurrentJam(); err != nil { - return nil - } - teamPath := []string{"jams", currJam, "teams"} + teamPath := []string{"teams"} var teamUids []string if teamUids, err = db.GetBucketList(teamPath); err != nil { for _, v := range teamUids { @@ -151,17 +124,24 @@ func dbGetTeamByName(nm string) *Team { return nil } +func dbUpdateTeam(id string, tm *Team) error { + var err error + if err = db.OpenDB(); err != nil { + return nil + } + defer db.CloseDB() + + teamPath := []string{"teams", id} + return db.SetValue(teamPath, "name", tm.Name) +} + func dbDeleteTeam(id string) error { var err error - var currJam string if err = db.OpenDB(); err != nil { return err } defer db.CloseDB() - if currJam, err = dbGetCurrentJam(); err != nil { - return err - } - teamPath := []string{"jams", currJam, "teams"} + teamPath := []string{"teams"} return db.DeleteBucket(teamPath, id) } diff --git a/pageSession.go b/page_session.go similarity index 80% rename from pageSession.go rename to page_session.go index a5fb7cd..c4e33ab 100644 --- a/pageSession.go +++ b/page_session.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/gorilla/sessions" + "github.com/pborman/uuid" ) // This is basically a convenience struct for @@ -30,6 +31,17 @@ func (p *pageSession) setStringValue(key, val string) { p.session.Save(p.req, p.w) } +func (p *pageSession) getClientID() string { + var clientId string + var err error + if clientId, err = p.getStringValue("client_id"); err != nil { + // No client id, generate and save one + clientId := uuid.New() + p.setStringValue("client_id", clientId) + } + return clientId +} + func (p *pageSession) setFlashMessage(msg, status string) { p.setStringValue("flash_message", msg) p.setStringValue("flash_status", status) diff --git a/publicEndpoints.go b/public_endpoints.go similarity index 88% rename from publicEndpoints.go rename to public_endpoints.go index 3eaa607..c72ca3d 100644 --- a/publicEndpoints.go +++ b/public_endpoints.go @@ -7,14 +7,18 @@ import ( func initPublicPage(w http.ResponseWriter, req *http.Request) *pageData { p := InitPageData(w, req) + return p } func handleMain(w http.ResponseWriter, req *http.Request) { page := initPublicPage(w, req) - page.SubTitle = "!" + page.SubTitle = "" + for _, tmpl := range []string{ "htmlheader.html", + "admin-menu.html", + "header.html", "main.html", "footer.html", "htmlfooter.html", diff --git a/templates/admin-activateclient.html b/templates/admin-activateclient.html new file mode 100644 index 0000000..9bb8028 --- /dev/null +++ b/templates/admin-activateclient.html @@ -0,0 +1,22 @@ +
+
+
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+
diff --git a/templates/admin-addteam.html b/templates/admin-addteam.html index 98e2f8a..6949e1f 100644 --- a/templates/admin-addteam.html +++ b/templates/admin-addteam.html @@ -3,7 +3,7 @@
- +
diff --git a/templates/admin-adduser.html b/templates/admin-adduser.html index adb26cc..914aa8f 100644 --- a/templates/admin-adduser.html +++ b/templates/admin-adduser.html @@ -3,7 +3,7 @@
- +
diff --git a/templates/admin-clients.html b/templates/admin-clients.html new file mode 100644 index 0000000..7b9fd2f --- /dev/null +++ b/templates/admin-clients.html @@ -0,0 +1,28 @@ + + + + + + + + + {{ range $i, $v := .TemplateData.Clients }} + + + + + + {{ end }} + + + diff --git a/templates/admin-editteam.html b/templates/admin-editteam.html index ae6a2a3..158f93b 100644 --- a/templates/admin-editteam.html +++ b/templates/admin-editteam.html @@ -7,7 +7,7 @@
- +
diff --git a/templates/admin-login.html b/templates/admin-login.html index 44bb3d4..25bd0a3 100644 --- a/templates/admin-login.html +++ b/templates/admin-login.html @@ -3,7 +3,7 @@
- +
diff --git a/templates/admin-menu.html b/templates/admin-menu.html index 9acc208..771e76c 100644 --- a/templates/admin-menu.html +++ b/templates/admin-menu.html @@ -1,4 +1,4 @@ -