+{{ end }}
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..12669f6
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,3 @@
+{{ define "page-content" }}
+ Test {{.foo}}
+{{ end }}
\ No newline at end of file
diff --git a/templates/share-add-user.html b/templates/share-add-user.html
new file mode 100644
index 0000000..7c9e93a
--- /dev/null
+++ b/templates/share-add-user.html
@@ -0,0 +1,22 @@
+{{ define "page-content" }}
+
+{{ end }}
\ No newline at end of file
diff --git a/templates/shares.html b/templates/shares.html
new file mode 100644
index 0000000..1231ebc
--- /dev/null
+++ b/templates/shares.html
@@ -0,0 +1,47 @@
+{{ define "page-styles"}}
+
+{{ end }}
+
+{{ define "page-content" }}
+
+{{ end }}
\ No newline at end of file
diff --git a/templates/users.html b/templates/users.html
new file mode 100644
index 0000000..32f6bb0
--- /dev/null
+++ b/templates/users.html
@@ -0,0 +1,16 @@
+{{ define "page-content" }}
+
+
+
+
User
+
Role
+
+
+
+ {{ range $user := .}}
+
{{ $user.Username }}
+
{{ $user.Role }}
+
{{ end }}
+
+
+{{ end }}
\ No newline at end of file
diff --git a/webadmin.go b/webadmin.go
new file mode 100644
index 0000000..d5d6a4c
--- /dev/null
+++ b/webadmin.go
@@ -0,0 +1,272 @@
+// 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)
+}