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" }}
-
-
-
- User |
- Role |
-
-
-
- {{ range $user := .Users }}
- {{ $user.Username }} |
- {{ $user.Role }} |
-
{{ end }}
-
-
+{{ define "page-styles"}}
+
+{{ end }}
+
+{{ define "page-content" }}
+
+
+
+ User |
+ Role |
+ Action |
+
+
+
+ {{ range $user := .Users }}
+
+ {{ $user.Username }} |
+ {{ $user.Role }} |
+
+
+ |
+
{{ end }}
+
+
+
+ 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