ui: /settings/users edit form + disable/enable/regenerate/force-logout
This commit is contained in:
@@ -267,6 +267,12 @@ func (s *Server) routes(r chi.Router) {
|
|||||||
r.Get("/settings/users", s.handleUIUsersList)
|
r.Get("/settings/users", s.handleUIUsersList)
|
||||||
r.Get("/settings/users/new", s.handleUIUserNewGet)
|
r.Get("/settings/users/new", s.handleUIUserNewGet)
|
||||||
r.Post("/settings/users/new", s.handleUIUserNewPost)
|
r.Post("/settings/users/new", s.handleUIUserNewPost)
|
||||||
|
r.Get("/settings/users/{id}/edit", s.handleUIUserEditGet)
|
||||||
|
r.Post("/settings/users/{id}/edit", s.handleUIUserEditPost)
|
||||||
|
r.Post("/settings/users/{id}/disable", s.handleUIUserDisablePost)
|
||||||
|
r.Post("/settings/users/{id}/enable", s.handleUIUserEnablePost)
|
||||||
|
r.Post("/settings/users/{id}/regenerate-setup", s.handleUIUserRegenerateSetupPost)
|
||||||
|
r.Post("/settings/users/{id}/force-logout", s.handleUIUserForceLogoutPost)
|
||||||
r.Get("/settings/users/{id}/setup-link", s.handleUIUserSetupLinkGet)
|
r.Get("/settings/users/{id}/setup-link", s.handleUIUserSetupLinkGet)
|
||||||
r.Get("/settings/notifications", s.handleUINotificationsList)
|
r.Get("/settings/notifications", s.handleUINotificationsList)
|
||||||
r.Get("/settings/notifications/new", s.handleUINotificationNewGet)
|
r.Get("/settings/notifications/new", s.handleUINotificationNewGet)
|
||||||
|
|||||||
@@ -210,6 +210,187 @@ func (s *Server) handleUIUserNewPost(w stdhttp.ResponseWriter, r *stdhttp.Reques
|
|||||||
stdhttp.StatusSeeOther)
|
stdhttp.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUIUserEditGet(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||||
|
u := s.requireUIUser(w, r)
|
||||||
|
if u == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
target, err := s.deps.Store.GetUserByID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
stdhttp.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
em := ""
|
||||||
|
if target.Email != nil {
|
||||||
|
em = *target.Email
|
||||||
|
}
|
||||||
|
view := s.baseView(r, u)
|
||||||
|
view.Title = "Edit user · restic-manager"
|
||||||
|
view.Active = "settings"
|
||||||
|
view.Page = userFormPage{
|
||||||
|
Mode: "edit", ID: target.ID, Username: target.Username,
|
||||||
|
Email: em, Role: string(target.Role),
|
||||||
|
Disabled: target.DisabledAt != nil,
|
||||||
|
}
|
||||||
|
_ = s.deps.UI.Render(w, "user_edit", view)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUIUserEditPost(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||||
|
u := s.requireUIUser(w, r)
|
||||||
|
if u == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
stdhttp.Error(w, "bad request", stdhttp.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
target, err := s.deps.Store.GetUserByID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
stdhttp.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
role, ok := validRole(r.PostForm.Get("role"))
|
||||||
|
if !ok {
|
||||||
|
stdhttp.Error(w, "bad role", stdhttp.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
email := strings.TrimSpace(r.PostForm.Get("email"))
|
||||||
|
if email != "" {
|
||||||
|
if _, err := mail.ParseAddress(email); err != nil {
|
||||||
|
stdhttp.Error(w, "bad email", stdhttp.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if target.Role == store.RoleAdmin && role != store.RoleAdmin && target.DisabledAt == nil {
|
||||||
|
n, _ := s.deps.Store.CountEnabledAdmins(r.Context())
|
||||||
|
if n <= 1 {
|
||||||
|
stdhttp.Error(w, "cannot demote last admin", stdhttp.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.deps.Store.SetUserRole(r.Context(), id, role); err != nil {
|
||||||
|
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.deps.Store.SetUserEmail(r.Context(), id, email); err != nil {
|
||||||
|
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
|
||||||
|
ID: ulid.Make().String(), UserID: &u.ID, Actor: "user",
|
||||||
|
Action: "user.updated", TargetKind: ptr("user"), TargetID: &id,
|
||||||
|
TS: time.Now().UTC(),
|
||||||
|
})
|
||||||
|
stdhttp.Redirect(w, r, "/settings/users", stdhttp.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUIUserDisablePost(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||||
|
u := s.requireUIUser(w, r)
|
||||||
|
if u == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
target, err := s.deps.Store.GetUserByID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
stdhttp.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if target.Role == store.RoleAdmin && target.DisabledAt == nil {
|
||||||
|
n, _ := s.deps.Store.CountEnabledAdmins(r.Context())
|
||||||
|
if n <= 1 {
|
||||||
|
stdhttp.Error(w, "cannot disable last admin", stdhttp.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
now := time.Now().UTC()
|
||||||
|
if err := s.deps.Store.DisableUser(r.Context(), id, now); err != nil {
|
||||||
|
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = s.deps.Store.DeleteSessionsByUserID(r.Context(), id)
|
||||||
|
_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
|
||||||
|
ID: ulid.Make().String(), UserID: &u.ID, Actor: "user",
|
||||||
|
Action: "user.disabled", TargetKind: ptr("user"), TargetID: &id,
|
||||||
|
TS: now,
|
||||||
|
})
|
||||||
|
stdhttp.Redirect(w, r, "/settings/users", stdhttp.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUIUserEnablePost(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||||
|
u := s.requireUIUser(w, r)
|
||||||
|
if u == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
if err := s.deps.Store.EnableUser(r.Context(), id); err != nil {
|
||||||
|
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
|
||||||
|
ID: ulid.Make().String(), UserID: &u.ID, Actor: "user",
|
||||||
|
Action: "user.enabled", TargetKind: ptr("user"), TargetID: &id,
|
||||||
|
TS: time.Now().UTC(),
|
||||||
|
})
|
||||||
|
stdhttp.Redirect(w, r, "/settings/users/"+id+"/edit", stdhttp.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUIUserRegenerateSetupPost(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||||
|
u := s.requireUIUser(w, r)
|
||||||
|
if u == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
if _, err := s.deps.Store.GetUserByID(r.Context(), id); err != nil {
|
||||||
|
stdhttp.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rawToken, err := generateSetupToken()
|
||||||
|
if err != nil {
|
||||||
|
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now().UTC()
|
||||||
|
if err := s.deps.Store.SetSetupToken(r.Context(), store.SetupToken{
|
||||||
|
UserID: id, TokenHash: hashSetupToken(rawToken),
|
||||||
|
ExpiresAt: now.Add(time.Hour), CreatedAt: now,
|
||||||
|
CreatedBy: &u.ID,
|
||||||
|
}); err != nil {
|
||||||
|
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = s.deps.Store.SetMustChangePassword(r.Context(), id, true)
|
||||||
|
_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
|
||||||
|
ID: ulid.Make().String(), UserID: &u.ID, Actor: "user",
|
||||||
|
Action: "user.setup_token.regenerated",
|
||||||
|
TargetKind: ptr("user"), TargetID: &id, TS: now,
|
||||||
|
})
|
||||||
|
stdhttp.Redirect(w, r,
|
||||||
|
"/settings/users/"+id+"/setup-link?token="+rawToken,
|
||||||
|
stdhttp.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUIUserForceLogoutPost(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||||
|
u := s.requireUIUser(w, r)
|
||||||
|
if u == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
_, err := s.deps.Store.DeleteSessionsByUserID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
|
||||||
|
ID: ulid.Make().String(), UserID: &u.ID, Actor: "user",
|
||||||
|
Action: "user.force_logout",
|
||||||
|
TargetKind: ptr("user"), TargetID: &id,
|
||||||
|
TS: time.Now().UTC(),
|
||||||
|
})
|
||||||
|
stdhttp.Redirect(w, r, "/settings/users/"+id+"/edit", stdhttp.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleUIUserSetupLinkGet(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
func (s *Server) handleUIUserSetupLinkGet(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||||
u := s.requireUIUser(w, r)
|
u := s.requireUIUser(w, r)
|
||||||
if u == nil {
|
if u == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user