diff --git a/cmd_user.go b/cmd_user.go index a930d1f..938ec4e 100644 --- a/cmd_user.go +++ b/cmd_user.go @@ -148,3 +148,26 @@ func (cmd CmdUserDelete) Run(app *app) error { } return nil } + +func (app *app) deleteUser(username string) error { + if err := app.userStore.RemoveUser(username); err != nil { + return fmt.Errorf("cannot remove user: %w", err) + } + + sharesByUser, err := app.shareStore.FindSharesByUser(username) + if err != nil { + return fmt.Errorf("cannot get shares of user: %w", err) + } + allSuccessful := true + for _, userShare := range sharesByUser { + if err := app.shareStore.RemoveShare(userShare.UUID); err != nil { + fmt.Fprintf(os.Stderr, "User %q cannot be removed from Share %q: %v\n", username, userShare.UUID.String(), err) + allSuccessful = false + } + } + if !allSuccessful { + return fmt.Errorf("could not remove user from all shares") + } + + return nil +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index ab233a0..a4b8a1c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -15,6 +15,7 @@ border: 0; background-color: #c4e1ff; font-weight: bold; + font-size: small; } input[type="submit"].delete { @@ -64,6 +65,32 @@ border: 1px solid gray; } + .dialog, .error { + left: 50%; + top: 50%; + position: absolute; + transform: translate(-50%, -50%); + border: solid lightgrey; + padding: 1em; + border-radius: .5em; + } + + .dialog label { + display: flex; + margin-bottom: 1em; + align-items: center; + } + + .form-content { + display: grid; + grid-gap : 20px; + grid-template-columns: 100px 1fr; + } + .form-controls { + margin-top: 1em; + text-align: right; + } + body { font-family: sans-serif; margin-left: 200px diff --git a/templates/create-user.html b/templates/create-user.html new file mode 100644 index 0000000..e64929e --- /dev/null +++ b/templates/create-user.html @@ -0,0 +1,21 @@ +{{ define "page-content" }} +
+
+ + + + + + + + + +
+
+ +
+
+{{ end }} \ No newline at end of file diff --git a/templates/users.html b/templates/users.html index 04e64d0..e23ea0f 100644 --- a/templates/users.html +++ b/templates/users.html @@ -1,16 +1,34 @@ -{{ define "page-content" }} - - - - - - - - - {{ range $user := .Users }} - - - {{ end }} - -
UserRole
{{ $user.Username }}{{ $user.Role }}
+{{ define "page-styles"}} + +{{ end }} + +{{ define "page-content" }} + + + + + + + + + + {{ range $user := .Users }} + + + + + {{ end }} + +
UserRoleAction
{{ $user.Username }}{{ $user.Role }} +
+ + +
+
+ + Create User {{ end }} \ No newline at end of file diff --git a/webadmin.go b/webadmin.go index 9aad1d3..3ad4384 100644 --- a/webadmin.go +++ b/webadmin.go @@ -54,6 +54,7 @@ type webAdminHandler struct { tplShareAddUser *template.Template tplShareAddLogin *template.Template tplCreateShare *template.Template + tplCreateUser *template.Template } type sessionContext struct { @@ -107,6 +108,7 @@ func newWebAdminHandler(app *app) *webAdminHandler { tplShareAddUser: loadTemplate("share-add-user"), tplShareAddLogin: loadTemplate("share-add-login"), tplCreateShare: loadTemplate("create-share"), + tplCreateUser: loadTemplate("create-user"), } r := chi.NewRouter() @@ -519,6 +521,85 @@ Are you sure you want to continue?`, share.UUID, share.Name) } sessionContext.Redirect("shares") }) + ar.Route("/create-user", func(r chi.Router) { + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + sessionContext := h.buildSessionContext(w, r) + + if sessionContext.user.Role != GlobalRoleAdmin { + sessionContext.Unauthorized() + return + } + + sessionContext.RenderPage(h.tplCreateUser, nil) + }) + r.Post("/", func(w http.ResponseWriter, r *http.Request) { + sessionContext := h.buildSessionContext(w, r) + + if sessionContext.user.Role != GlobalRoleAdmin { + sessionContext.Unauthorized() + return + } + + returnURL := "users" + + pwHash, err := bcrypt.GenerateFromPassword([]byte(r.FormValue("password")), bcrypt.DefaultCost) + if err != nil { + sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), returnURL) + return + } + + user := User{ + Username: r.FormValue("username"), + Password: string(pwHash), + Role: GlobalRole(r.FormValue("role")), + } + + switch user.Role { + case GlobalRoleUser: + case GlobalRoleAdmin: + default: + sessionContext.RenderError(template.HTML("Invalid role."), "") + return + } + + err = app.userStore.AddUser(user) + if err != nil { + sessionContext.RenderError(template.HTML("Cannot create user: "+err.Error()), "") + return + } + + sessionContext.Redirect("users#user-" + user.Username) + }) + }) + ar.Post("/delete-user", func(w http.ResponseWriter, r *http.Request) { + sessionContext := h.buildSessionContext(w, r) + + if sessionContext.user.Role != GlobalRoleAdmin { + sessionContext.Unauthorized() + return + } + + user, err := app.userStore.GetUser(r.FormValue("user")) + if err != nil { + sessionContext.RenderError(template.HTML("Cannot delete user: "+err.Error()), "users") + return + } + + message := fmt.Sprintf(`You are about to delete the user %s (%s).
+This will delete all share associations and logins.

+Are you sure you want to continue?`, user.Username, user.Role) + if confirmStatus := sessionContext.RequestConfirmation(template.HTML(message), "delete-user"); confirmStatus == confirmRequested { + // We have already rendered. Nothing to do. + return + } else if confirmStatus == confirmAccepted { + cmd := CmdUserDelete{Username: user.Username} + if err := cmd.Run(app); err != nil { + sessionContext.RenderError(template.HTML("Cannot delete user: "+err.Error()), "users") + return + } + } + sessionContext.Redirect("users") + }) h.router = r return h