From 49b0a3530448e3d78f8244705a7178c383a88417 Mon Sep 17 00:00:00 2001 From: handlerug Date: Fri, 16 Jul 2021 01:58:27 +0700 Subject: [PATCH] Refactor admin routes They're not perfect, I still don't like them, but I can't think of a good solution right now. I'm going to thinking about the best way of doing web stuff for some time. --- web/admin.go | 273 +++++++++++++++++++++++---------------------------- web/web.go | 41 ++++++-- 2 files changed, 154 insertions(+), 160 deletions(-) diff --git a/web/admin.go b/web/admin.go index da16524..dbaf960 100644 --- a/web/admin.go +++ b/web/admin.go @@ -2,12 +2,12 @@ package web import ( "fmt" + "os" "io" "log" "mime" "net/http" "sort" - "strings" "github.com/gorilla/mux" @@ -19,181 +19,154 @@ import ( // initAdmin sets up /admin routes if auth is used. Call it after you have decided if you want to use auth. func initAdmin(r *mux.Router) { - if cfg.UseAuth { - r.HandleFunc("/admin/shutdown", handlerAdminShutdown) - r.HandleFunc("/admin/reindex-users", handlerAdminReindexUsers) + r.HandleFunc("/shutdown", handlerAdminShutdown).Methods(http.MethodPost) + r.HandleFunc("/reindex-users", handlerAdminReindexUsers).Methods(http.MethodPost) - r.PathPrefix("/admin/users/").HandlerFunc(handlerAdminUsers) - r.HandleFunc("/admin/user/new", handlerAdminUserNew) - r.HandleFunc("/admin", handlerAdmin) - } + r.HandleFunc("/user/new", handlerAdminUserNew).Methods(http.MethodGet, http.MethodPost) + r.HandleFunc("/users/{username}/edit", handlerAdminUserEdit).Methods(http.MethodGet, http.MethodPost) + r.HandleFunc("/users/{username}/delete", handlerAdminUserDelete).Methods(http.MethodGet, http.MethodPost) + r.HandleFunc("/users", handlerAdminUsers) + + r.HandleFunc("/", handlerAdmin) } // handlerAdmin provides the admin panel. func handlerAdmin(w http.ResponseWriter, rq *http.Request) { - util.PrepareRq(rq) - if user.CanProceed(rq, "admin") { - w.Header().Set("Content-Type", "text/html;charset=utf-8") - w.WriteHeader(http.StatusOK) - _, err := io.WriteString(w, views.BaseHTML("Admin panel", views.AdminPanelHTML(), user.FromRequest(rq))) - if err != nil { - log.Println(err) - } - } + w.Header().Set("Content-Type", "text/html;charset=utf-8") + w.WriteHeader(http.StatusOK) + io.WriteString(w, views.BaseHTML("Admin panel", views.AdminPanelHTML(), user.FromRequest(rq))) } // handlerAdminShutdown kills the wiki. func handlerAdminShutdown(w http.ResponseWriter, rq *http.Request) { - util.PrepareRq(rq) - if user.CanProceed(rq, "admin/shutdown") && rq.Method == "POST" { - log.Fatal("An admin commanded the wiki to shutdown") + if user.CanProceed(rq, "admin/shutdown") { + log.Println("An admin commanded the wiki to shutdown") + os.Exit(0) } } // handlerAdminReindexUsers reinitialises the user system. func handlerAdminReindexUsers(w http.ResponseWriter, rq *http.Request) { - util.PrepareRq(rq) - if user.CanProceed(rq, "admin") && rq.Method == "POST" { - user.ReadUsersFromFilesystem() - redirectTo := rq.Referer() - if redirectTo == "" { - redirectTo = "/hypha/" + cfg.UserHypha - } - http.Redirect(w, rq, redirectTo, http.StatusSeeOther) + user.ReadUsersFromFilesystem() + redirectTo := rq.Referer() + if redirectTo == "" { + redirectTo = "/hypha/" + cfg.UserHypha } + http.Redirect(w, rq, redirectTo, http.StatusSeeOther) } func handlerAdminUsers(w http.ResponseWriter, rq *http.Request) { - util.PrepareRq(rq) - if user.CanProceed(rq, "admin") { - path := strings.TrimPrefix(rq.URL.Path, "/admin/users") - parts := strings.Split(path, "/")[1:] - - // Users dashboard - if len(parts) == 0 { - // Get a sorted list of users - var userList []*user.User - for u := range user.YieldUsers() { - userList = append(userList, u) - } - - sort.Slice(userList, func(i, j int) bool { - less := userList[i].RegisteredAt.Before(userList[j].RegisteredAt) - return less - }) - - html := views.AdminUsersPanelHTML(userList) - html = views.BaseHTML("Manage users", html, user.FromRequest(rq)) - - w.Header().Set("Content-Type", mime.TypeByExtension(".html")) - if _, err := io.WriteString(w, html); err != nil { - log.Println(err) - } - return - } - - if len(parts) != 2 { - util.HTTP404Page(w, "404 page not found") - return - } - - u := user.UserByName(parts[0]) - if u == nil { - util.HTTP404Page(w, "404 page not found") - return - } - - switch parts[1] { - case "edit": - f := util.FormDataFromRequest(rq, []string{"group"}) - - if rq.Method == http.MethodPost { - oldGroup := u.Group - newGroup := f.Get("group") - - if user.ValidGroup(newGroup) { - u.Group = newGroup - if err := user.SaveUserDatabase(); err != nil { - u.Group = oldGroup - log.Println(err) - f = f.WithError(err) - } else { - http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther) - return - } - } else { - f = f.WithError(fmt.Errorf("invalid group \"%s\"", newGroup)) - } - } - - f.Put("group", u.Group) - - html := views.AdminUserEditHTML(u, f) - html = views.BaseHTML(fmt.Sprintf("User %s", u.Name), html, user.FromRequest(rq)) - - if f.HasError() { - w.WriteHeader(http.StatusBadRequest) - } - w.Header().Set("Content-Type", mime.TypeByExtension(".html")) - io.WriteString(w, html) - return - case "delete": - f := util.NewFormData() - - if rq.Method == http.MethodPost { - f = f.WithError(user.DeleteUser(u.Name)) - if !f.HasError() { - http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther) - } else { - log.Println(f.Error()) - } - } - - html := views.AdminUserDeleteHTML(u, util.NewFormData()) - html = views.BaseHTML(fmt.Sprintf("User %s", u.Name), html, user.FromRequest(rq)) - - if f.HasError() { - w.WriteHeader(http.StatusBadRequest) - } - w.Header().Set("Content-Type", mime.TypeByExtension(".html")) - io.WriteString(w, html) - return - } - - util.HTTP404Page(w, "404 page not found") + // Get a sorted list of users + var userList []*user.User + for u := range user.YieldUsers() { + userList = append(userList, u) } + + sort.Slice(userList, func(i, j int) bool { + less := userList[i].RegisteredAt.Before(userList[j].RegisteredAt) + return less + }) + + html := views.AdminUsersPanelHTML(userList) + html = views.BaseHTML("Manage users", html, user.FromRequest(rq)) + + w.Header().Set("Content-Type", mime.TypeByExtension(".html")) + io.WriteString(w, html) +} + +func handlerAdminUserEdit(w http.ResponseWriter, rq *http.Request) { + vars := mux.Vars(rq) + u := user.UserByName(vars["username"]) + if u == nil { + util.HTTP404Page(w, "404 page not found") + return + } + + f := util.FormDataFromRequest(rq, []string{"group"}) + + if rq.Method == http.MethodPost { + oldGroup := u.Group + newGroup := f.Get("group") + + if user.ValidGroup(newGroup) { + u.Group = newGroup + if err := user.SaveUserDatabase(); err != nil { + u.Group = oldGroup + log.Println(err) + f = f.WithError(err) + } else { + http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther) + return + } + } else { + f = f.WithError(fmt.Errorf("invalid group \"%s\"", newGroup)) + } + } + + f.Put("group", u.Group) + + html := views.AdminUserEditHTML(u, f) + html = views.BaseHTML(fmt.Sprintf("User %s", u.Name), html, user.FromRequest(rq)) + + if f.HasError() { + w.WriteHeader(http.StatusBadRequest) + } + w.Header().Set("Content-Type", mime.TypeByExtension(".html")) + io.WriteString(w, html) +} + +func handlerAdminUserDelete(w http.ResponseWriter, rq *http.Request) { + vars := mux.Vars(rq) + u := user.UserByName(vars["username"]) + if u == nil { + util.HTTP404Page(w, "404 page not found") + return + } + + f := util.NewFormData() + + if rq.Method == http.MethodPost { + f = f.WithError(user.DeleteUser(u.Name)) + if !f.HasError() { + http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther) + } else { + log.Println(f.Error()) + } + } + + html := views.AdminUserDeleteHTML(u, util.NewFormData()) + html = views.BaseHTML(fmt.Sprintf("User %s", u.Name), html, user.FromRequest(rq)) + + if f.HasError() { + w.WriteHeader(http.StatusBadRequest) + } + w.Header().Set("Content-Type", mime.TypeByExtension(".html")) + io.WriteString(w, html) } func handlerAdminUserNew(w http.ResponseWriter, rq *http.Request) { - util.PrepareRq(rq) - if user.CanProceed(rq, "admin") { - if rq.Method == http.MethodGet { - // New user form - html := views.AdminUserNewHTML(util.NewFormData()) + if rq.Method == http.MethodGet { + // New user form + html := views.AdminUserNewHTML(util.NewFormData()) + html = views.BaseHTML("New user", html, user.FromRequest(rq)) + + w.Header().Set("Content-Type", mime.TypeByExtension(".html")) + io.WriteString(w, html) + } else if rq.Method == http.MethodPost { + // Create a user + f := util.FormDataFromRequest(rq, []string{"name", "password", "group"}) + + err := user.Register(f.Get("name"), f.Get("password"), f.Get("group"), "local", true) + + if err != nil { + html := views.AdminUserNewHTML(f.WithError(err)) html = views.BaseHTML("New user", html, user.FromRequest(rq)) + w.WriteHeader(http.StatusBadRequest) w.Header().Set("Content-Type", mime.TypeByExtension(".html")) io.WriteString(w, html) - return - } else if rq.Method == http.MethodPost { - // Create a user - f := util.FormDataFromRequest(rq, []string{"name", "password", "group"}) - - err := user.Register(f.Get("name"), f.Get("password"), f.Get("group"), "local", true) - - if err != nil { - html := views.AdminUserNewHTML(f.WithError(err)) - html = views.BaseHTML("New user", html, user.FromRequest(rq)) - - w.WriteHeader(http.StatusBadRequest) - w.Header().Set("Content-Type", mime.TypeByExtension(".html")) - io.WriteString(w, html) - } else { - http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther) - } - return + } else { + http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther) } } - - util.HTTP404Page(w, "404 page not found") } diff --git a/web/web.go b/web/web.go index 5e93eb0..ce736fe 100644 --- a/web/web.go +++ b/web/web.go @@ -65,8 +65,7 @@ func handlerRobotsTxt(w http.ResponseWriter, rq *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") file, err := static.FS.Open("robots.txt") - if err != nil { - return + if err != nil { return } io.Copy(w, file) file.Close() @@ -75,22 +74,21 @@ func handlerRobotsTxt(w http.ResponseWriter, rq *http.Request) { func Handler() http.Handler { router := mux.NewRouter() router.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Do stuff here - log.Println(r.RequestURI) - // Call the next handler, which can be another middleware in the chain, or the final handler. - next.ServeHTTP(w, r) + return http.HandlerFunc(func(w http.ResponseWriter, rq *http.Request) { + util.PrepareRq(rq) + next.ServeHTTP(w, rq) }) }) + router.StrictSlash(true) - // Public routes + // Public routes. They're always accessible regardless of the user status. initAuth(router) router.HandleFunc("/robots.txt", handlerRobotsTxt) router.HandleFunc("/static/style.css", handlerStyle) router.PathPrefix("/static/"). Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.FS)))) - // Wiki routes. They may be locked or restricted + // Wiki routes. They may be locked or restricted. wikiRouter := router.PathPrefix("").Subrouter() wikiRouter.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, rq *http.Request) { @@ -103,11 +101,17 @@ func Handler() http.Handler { initReaders(wikiRouter) initMutators(wikiRouter) - initAdmin(wikiRouter) initHistory(wikiRouter) initStuff(wikiRouter) initSearch(wikiRouter) + // Admin routes. + if cfg.UseAuth { + adminRouter := wikiRouter.PathPrefix("/admin").Subrouter() + adminRouter.Use(groupMiddleware("admin")) + initAdmin(adminRouter) + } + // Miscellaneous wikiRouter.HandleFunc("/user-list", handlerUserList) @@ -121,3 +125,20 @@ func Handler() http.Handler { return router } + +func groupMiddleware(group string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, rq *http.Request) { + if cfg.UseAuth && user.CanProceed(rq, group) { + next.ServeHTTP(w, rq) + return + } + + // TODO: handle this better. Merge this code with all other + // authorization code in this project. + + w.WriteHeader(http.StatusForbidden) + io.WriteString(w, "403 forbidden") + }) + } +}