// Copyright (c) 2020, Andreas Schneider // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of the nor the // names of its contributors may be used to endorse or promote products // derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package main import ( "fmt" "html/template" "net/http" "path" "github.com/go-chi/chi" uuid "github.com/satori/go.uuid" ) type webAdminHandler struct { router chi.Router tplError *template.Template tplConfirm *template.Template tplIndex *template.Template tplUsers *template.Template tplShares *template.Template tplShareAddUser *template.Template tplCreateShare *template.Template } func newWebAdminHandler(app *app) *webAdminHandler { loadTemplate := func(filename string) *template.Template { t, err := template.ParseFiles( path.Join("templates", "base.html"), path.Join("templates", filename+".html"), ) if err != nil { panic(fmt.Sprintf("cannot load template %q: %v", filename, err)) } return t } h := &webAdminHandler{ tplError: loadTemplate("error"), tplConfirm: loadTemplate("confirm"), tplIndex: loadTemplate("index"), tplUsers: loadTemplate("users"), tplShares: loadTemplate("shares"), tplShareAddUser: loadTemplate("share-add-user"), tplCreateShare: loadTemplate("create-share"), } renderPage := func(w http.ResponseWriter, tmpl *template.Template, model interface{}) { if err := tmpl.Execute(w, model); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } renderError := func(w http.ResponseWriter, msg, returnURL string) { model := struct { ErrorMessage string ReturnURL string }{ ErrorMessage: msg, ReturnURL: returnURL, } renderPage(w, h.tplError, model) } const confirmRequested = 0 const confirmAccepted = 1 const confirmDenied = 2 confirm := func(w http.ResponseWriter, r *http.Request, msg template.HTML, returnURL string) int { if r.FormValue("_yes") != "" { return confirmAccepted } else if r.FormValue("_no") != "" { return confirmDenied } else { model := struct { URL string Fields map[string]string Message template.HTML }{ URL: returnURL, Fields: make(map[string]string), Message: msg, } for k, v := range r.Form { model.Fields[k] = v[0] } renderPage(w, h.tplConfirm, model) return confirmRequested } } r := chi.NewRouter() r.Get("/", func(w http.ResponseWriter, r *http.Request) { renderPage(w, h.tplIndex, map[string]string{"foo": "bar"}) }) r.Get("/users", func(w http.ResponseWriter, r *http.Request) { users, err := app.userStore.GetUsers() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } renderPage(w, h.tplUsers, users) }) r.Get("/shares", func(w http.ResponseWriter, r *http.Request) { shares, err := app.shareStore.GetShares() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } type shareInfo struct { Share Users []ShareUser } shareInfos := make([]shareInfo, len(shares)) for i := range shares { shareInfos[i].Share = shares[i] users, err := app.shareStore.GetShareUsers(shares[i]) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } shareInfos[i].Users = users } renderPage(w, h.tplShares, shareInfos) }) r.Route("/share-add-user", func(r chi.Router) { r.Get("/", func(w http.ResponseWriter, r *http.Request) { shareId := r.URL.Query().Get("share") if shareId == "" { http.Error(w, "invalid share id", http.StatusBadRequest) return } users, err := app.userStore.GetUsers() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } model := map[string]interface{}{ "ShareId": shareId, "Users": users, } renderPage(w, h.tplShareAddUser, model) }) r.Post("/", func(w http.ResponseWriter, r *http.Request) { returnURL := "share-add-user?share=" + r.FormValue("share") shareId, err := uuid.FromString(r.FormValue("share")) if err != nil { renderError(w, "Internal error: "+err.Error(), "") return } share := Share{UUID: shareId} user, err := app.userStore.GetUser(r.FormValue("user")) if err == ErrUserNotFound { renderError(w, "User not found.", returnURL) return } else if err != nil { renderError(w, "Internal error: "+err.Error(), "") return } err = app.shareStore.AddUserToShare(share, user.Username, ShareRole(r.FormValue("role"))) if err != nil { renderError(w, "Cannot add user to share: "+err.Error(), returnURL) return } http.Redirect(w, r, "shares", http.StatusFound) }) }) r.Post("/share-delete-user", func(w http.ResponseWriter, r *http.Request) { returnURL := "shares" shareId, err := uuid.FromString(r.FormValue("share")) if err != nil { renderError(w, "Internal error: "+err.Error(), "") return } share := Share{UUID: shareId} err = app.shareStore.RemoveUserFromShare(share, r.FormValue("user")) if err != nil { renderError(w, "Cannot remove user from share: "+err.Error(), returnURL) return } http.Redirect(w, r, "shares", http.StatusFound) }) r.Route("/create-share", func(r chi.Router) { r.Get("/", func(w http.ResponseWriter, r *http.Request) { renderPage(w, h.tplCreateShare, nil) }) r.Post("/", func(w http.ResponseWriter, r *http.Request) { share, err := app.shareStore.CreateShare() if err != nil { renderError(w, "Cannot create share: "+err.Error(), "") return } share.Name = r.FormValue("name") share.Description = r.FormValue("description") if err := app.shareStore.UpdateShareAttributes(share); err != nil { renderError(w, "Cannot update share: "+err.Error(), "") return } http.Redirect(w, r, "shares#share-"+share.UUID.String(), http.StatusFound) }) }) r.Post("/delete-share", func(w http.ResponseWriter, r *http.Request) { share, err := app.shareStore.GetShare(r.FormValue("share")) if err != nil { renderError(w, "Internal error: "+err.Error(), "") return } message := fmt.Sprintf(`You are about to delete the share %s (%s).
This will delete all data permanently.

Are you sure you want to continue?`, share.UUID, share.Name) if confirmStatus := confirm(w, r, template.HTML(message), "delete-share"); confirmStatus == confirmRequested { // We have already rendered. Nothing to do. return } else if confirmStatus == confirmAccepted { if err := app.shareStore.RemoveShare(share.UUID); err != nil { renderError(w, "Share cannot be removed: "+err.Error(), "shares") return } } http.Redirect(w, r, "shares", http.StatusFound) }) h.router = r return h } func (h webAdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.router.ServeHTTP(w, r) }