~handlerug/mycorrhiza

49b0a3530448e3d78f8244705a7178c383a88417 — handlerug 19 days ago 3ee21e3
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.
2 files changed, 140 insertions(+), 146 deletions(-)

M web/admin.go
M web/web.go
M web/admin.go => web/admin.go +109 -136
@@ 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)
			}
	// 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
			})
	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))
	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
		}
	w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
	io.WriteString(w, html)
}

		if len(parts) != 2 {
			util.HTTP404Page(w, "404 page not found")
			return
		}
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
	}

		u := user.UserByName(parts[0])
		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")

		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))
				}
		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)
	f.Put("group", u.Group)

			html := views.AdminUserEditHTML(u, f)
			html = views.BaseHTML(fmt.Sprintf("User %s", u.Name), html, user.FromRequest(rq))
	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())
				}
			}
	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
	}

			html := views.AdminUserDeleteHTML(u, util.NewFormData())
			html = views.BaseHTML(fmt.Sprintf("User %s", u.Name), html, user.FromRequest(rq))
	f := util.NewFormData()

			if f.HasError() {
				w.WriteHeader(http.StatusBadRequest)
			}
			w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
			io.WriteString(w, html)
			return
	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())
		}
	}

		util.HTTP404Page(w, "404 page not found")
	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())
			html = views.BaseHTML("New user", html, user.FromRequest(rq))
	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)
			return
		} else if rq.Method == http.MethodPost {
			// Create a user
			f := util.FormDataFromRequest(rq, []string{"name", "password", "group"})
		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)
		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))
		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
			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)
		}
	}

	util.HTTP404Page(w, "404 page not found")
}

M web/web.go => web/web.go +31 -10
@@ 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")
		})
	}
}