This thread contains a patchset. You're looking at the original emails,
but you may wish to use the patch review UI.
Review patch
5
2
[PATCH 0/4] implement changing password
This implements changing passwords for users by the admin or users
themselves.
Feature request initially at
https://github.com/bouncepaw/mycorrhiza/issues/211
Does not implement the well-known endpoint for changing passwords, as
the spec says "It is provided for discussion only and may change at any
moment." (https://w3c.github.io/webappsec-change-password-url/ )
The code quality is very much not up to scratch, so suggest improvements
if there is any. The code has been tested and it functions.
Jackson (4):
implement changing user password function
move form errors out of change group thing
implement admin form to change a user's password
implement user facing password change page
admin/admin.go | 43 +++++++++++++++++++++
admin/view.go | 5 +++
admin/view_edit_user.html | 24 +++++++++++-
settings/settings.go | 62 ++++++++++++++++++++++++++++++
settings/view.go | 47 ++++++++++++++++++++++
settings/view_change_password.html | 37 ++++++++++++++++++
user/user.go | 15 ++++++++
web/web.go | 6 +++
8 files changed, 237 insertions(+), 2 deletions(-)
create mode 100644 settings/settings.go
create mode 100644 settings/view.go
create mode 100644 settings/view_change_password.html
--
2.43.0
[PATCH 2/4] move form errors out of change group thing
there are multiple form fields now, so the error could apply to any one
of the forms
---
admin/view_edit_user.html | 4 ++ --
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/admin/view_edit_user.html b/admin/view_edit_user.html
index 6d804a9..c6d6993 100644
--- a/admin/view_edit_user.html
+++ b/admin/view_edit_user.html
@@ -7,8 +7,6 @@
{{.U.Name}}
</h1>
- <h2>{{block "change group" .}}Change group{{end}}</h2>
-
{{if .Form.HasError}}
<div class="notice notice--error">
<strong>{{template "error"}}:</strong>
@@ -16,6 +14,8 @@
</div>
{{end}}
+ <h2>{{block "change group" .}}Change group{{end}}</h2>
+
<form action="" method="post">
<div class="form-field">
<select id="group" name="group" aria-label="{{block "group" .}}Group{{end}}">
--
2.43.0
[PATCH 1/4] implement changing user password function
---
user/user.go | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/user/user.go b/user/user.go
index 2854e6d..72a2c25 100644
--- a/user/user.go
+++ b/user/user.go
@@ -1,6 +1,7 @@
package user
import (
+ "fmt"
"net/http"
"strings"
"sync"
@@ -135,6 +136,20 @@ func (user *User) ShowLockMaybe(w http.ResponseWriter, rq *http.Request) bool {
return false
}
+ // Sets a new password for the user.
+ func (user *User) ChangePassword(password string) error {
+ if user.Source != "local" {
+ return fmt.Errorf("Only local users can change their passwords.")
+ }
+
+ hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+ if err != nil {
+ return err
+ }
+ user.Password = string(hash)
+ return SaveUserDatabase()
+ }
+
// IsValidUsername checks if the given username is valid.
func IsValidUsername(username string) bool {
for _, r := range username {
--
2.43.0
[PATCH 3/4] implement admin form to change a user's password
---
Russian strings left untranslated.
admin/admin.go | 43 +++++++++++++++++++++++++++++++++++++++
admin/view.go | 5 +++++
admin/view_edit_user.html | 20 ++++++++++++++++++
3 files changed, 68 insertions(+)
diff --git a/admin/admin.go b/admin/admin.go
index ef05f05..ef5ac3d 100644
--- a/admin/admin.go
+++ b/admin/admin.go
@@ -93,6 +93,49 @@ func handlerAdminUserEdit(w http.ResponseWriter, rq *http.Request) {
viewEditUser(viewutil.MetaFrom(w, rq), f, u)
}
+ func handlerAdminUserChangePassword(w http.ResponseWriter, rq *http.Request) {
+ vars := mux.Vars(rq)
+ u := user.ByName(vars["username"])
+ if u == nil {
+ util.HTTP404Page(w, "404 page not found")
+ return
+ }
+
+ f := util.FormDataFromRequest(rq, []string{"password", "password_confirm"})
+
+ password := f.Get("password")
+ passwordConfirm := f.Get("password_confirm")
+ // server side validation
+ if password == "" {
+ err := fmt.Errorf("passwords should not be empty")
+ f = f.WithError(err)
+ }
+ if password == passwordConfirm {
+ previousPassword := u.Password // for rollback
+ if err := u.ChangePassword(password); err != nil {
+ f = f.WithError(err)
+ } else {
+ if err := user.SaveUserDatabase(); err != nil {
+ u.Password = previousPassword
+ f = f.WithError(err)
+ } else {
+ http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
+ return
+ }
+ }
+ } else {
+ err := fmt.Errorf("passwords do not match")
+ f = f.WithError(err)
+ }
+
+ if f.HasError() {
+ w.WriteHeader(http.StatusBadRequest)
+ }
+ w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
+
+ viewEditUser(viewutil.MetaFrom(w, rq), f, u)
+ }
+
func handlerAdminUserDelete(w http.ResponseWriter, rq *http.Request) {
vars := mux.Vars(rq)
u := user.ByName(vars["username"])
diff --git a/admin/view.go b/admin/view.go
index d3ce063..e7537c9 100644
--- a/admin/view.go
+++ b/admin/view.go
@@ -10,6 +10,7 @@ import (
"net/http"
)
+ // TODO: translate some untranslated strings
const adminTranslationRu = `
{{define "panel title"}}Панель админстратора{{end}}
{{define "panel safe section title"}}Безопасная секция{{end}}
@@ -33,6 +34,9 @@ const adminTranslationRu = `
{{define "new user"}}Новый пользователь{{end}}
{{define "password"}}Пароль{{end}}
+ {{define "confirm password"}}Confirm password{{end}}
+ {{define "change password"}}Change password{{end}}
+ {{define "non local password change"}}Non-local accounts cannot have their passwords changed.{{end}}
{{define "create"}}Создать{{end}}
{{define "change group"}}Изменить группу{{end}}
@@ -57,6 +61,7 @@ func Init(rtr *mux.Router) {
rtr.HandleFunc("/new-user", handlerAdminUserNew).Methods(http.MethodGet, http.MethodPost)
rtr.HandleFunc("/users/{username}/edit", handlerAdminUserEdit).Methods(http.MethodGet, http.MethodPost)
+ rtr.HandleFunc("/users/{username}/change-password", handlerAdminUserChangePassword).Methods(http.MethodPost)
rtr.HandleFunc("/users/{username}/delete", handlerAdminUserDelete).Methods(http.MethodGet, http.MethodPost)
rtr.HandleFunc("/users", handlerAdminUsers)
diff --git a/admin/view_edit_user.html b/admin/view_edit_user.html
index c6d6993..63d99f4 100644
--- a/admin/view_edit_user.html
+++ b/admin/view_edit_user.html
@@ -33,6 +33,26 @@
</div>
</form>
+ <h2>{{block "change password" .}}Change password{{end}}</h2>
+
+ {{if eq .U.Source "local"}}
+ <form action="/admin/users/{{.U.Name}}/change-password" method="post">
+ <div class="form-field">
+ <label for="pass">{{block "password" .}}Password{{end}}</label>
+ <input required type="password" autocomplete="new-password" id="pass" name="password">
+ <br>
+ <label for="pass_confirm">{{block "confirm password" .}}Confirm password{{end}}</label>
+ <input required type="password" autocomplete="new-password" id="pass_confirm" name="password_confirm">
+ </div>
+
+ <div class="form-field">
+ <input class="btn" type="submit" value='{{block "submit" .}}Submit{{end}}'>
+ </div>
+ </form>
+ {{else}}
+ <p>{{block "non local password change" .}}Non-local accounts cannot have their passwords changed.{{end}}</p>
+ {{end}}
+
<h2>{{block "delete user" .}}Delete user{{end}}</h2>
<p>{{block "delete user tip" .}}Remove the user from the database. Changes made by the user will be preserved. It will be possible to take this username later.{{end}}</p>
<a class="btn btn_destructive" href="/admin/users/{{.U.Name}}/delete">{{template "delete"}}</a>
--
2.43.0
[PATCH 4/4] implement user facing password change page
similar to the admin password change, but with a few changes:
- require current password verification
the following still included:
- empty password check
- confirm password check
---
Russian strings left untranslated.
settings/settings.go | 62 ++++++++++++++++++++++++++++++
settings/view.go | 47 ++++++++++++++++++++++
settings/view_change_password.html | 37 ++++++++++++++++++
web/web.go | 6 +++
4 files changed, 152 insertions(+)
create mode 100644 settings/settings.go
create mode 100644 settings/view.go
create mode 100644 settings/view_change_password.html
diff --git a/settings/settings.go b/settings/settings.go
new file mode 100644
index 0000000..fbb3eca
--- /dev/null
+++ b/settings/settings.go
@@ -0,0 +1,62 @@
+ package settings
+
+ import (
+ "fmt"
+ "mime"
+ "net/http"
+ "reflect"
+
+ "github.com/bouncepaw/mycorrhiza/viewutil"
+
+ "github.com/bouncepaw/mycorrhiza/user"
+ "github.com/bouncepaw/mycorrhiza/util"
+ )
+
+ func handlerUserChangePassword(w http.ResponseWriter, rq *http.Request) {
+ u := user.FromRequest(rq)
+ // TODO: is there a better way?
+ if reflect.DeepEqual(u, user.EmptyUser()) || u == nil {
+ util.HTTP404Page(w, "404 page not found")
+ return
+ }
+
+ f := util.FormDataFromRequest(rq, []string{"current_password", "password", "password_confirm"})
+ currentPassword := f.Get("current_password")
+
+ if user.CredentialsOK(u.Name, currentPassword) {
+ password := f.Get("password")
+ passwordConfirm := f.Get("password_confirm")
+ // server side validation
+ if password == "" {
+ err := fmt.Errorf("passwords should not be empty")
+ f = f.WithError(err)
+ }
+ if password == passwordConfirm {
+ previousPassword := u.Password // for rollback
+ if err := u.ChangePassword(password); err != nil {
+ f = f.WithError(err)
+ } else {
+ if err := user.SaveUserDatabase(); err != nil {
+ u.Password = previousPassword
+ f = f.WithError(err)
+ } else {
+ http.Redirect(w, rq, "/", http.StatusSeeOther)
+ return
+ }
+ }
+ } else {
+ err := fmt.Errorf("passwords do not match")
+ f = f.WithError(err)
+ }
+ } else {
+ err := fmt.Errorf("incorrect password")
+ f = f.WithError(err)
+ }
+
+ if f.HasError() {
+ w.WriteHeader(http.StatusBadRequest)
+ }
+ w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
+
+ changePasswordPage(viewutil.MetaFrom(w, rq), f, u)
+ }
diff --git a/settings/view.go b/settings/view.go
new file mode 100644
index 0000000..338447c
--- /dev/null
+++ b/settings/view.go
@@ -0,0 +1,47 @@
+ package settings
+
+ import (
+ "embed"
+ "github.com/bouncepaw/mycorrhiza/util"
+ "github.com/bouncepaw/mycorrhiza/viewutil"
+ "github.com/gorilla/mux"
+ "net/http"
+
+ "github.com/bouncepaw/mycorrhiza/user"
+ )
+
+ // TODO: translate untranslated strings
+ const settingsTranslationRu = `
+ {{define "change password"}}Change password{{end}}
+ {{define "confirm password"}}Confirm password{{end}}
+ {{define "current password"}}Current password{{end}}
+ {{define "non local password change"}}Non-local accounts cannot have their passwords changed.{{end}}
+ {{define "password"}}Password{{end}}
+ {{define "submit"}}Submit{{end}}
+ `
+
+ var (
+ //go:embed *.html
+ fs embed.FS
+ changePassowrdChain viewutil.Chain
+ )
+
+ func Init(rtr *mux.Router) {
+ rtr.HandleFunc("/change-password", handlerUserChangePassword).Methods(http.MethodGet, http.MethodPost)
+
+ changePassowrdChain = viewutil.CopyEnRuWith(fs, "view_change_password.html", settingsTranslationRu)
+ }
+
+ func changePasswordPage(meta viewutil.Meta, form util.FormData, u *user.User) {
+ viewutil.ExecutePage(meta, changePassowrdChain, changePasswordData{
+ BaseData: &viewutil.BaseData{},
+ Form: form,
+ U: u,
+ })
+ }
+
+ type changePasswordData struct {
+ *viewutil.BaseData
+ Form util.FormData
+ U *user.User
+ }
diff --git a/settings/view_change_password.html b/settings/view_change_password.html
new file mode 100644
index 0000000..26a15af
--- /dev/null
+++ b/settings/view_change_password.html
@@ -0,0 +1,37 @@
+ {{/* TODO: translate title? */}}
+ {{define "title"}}Change password{{end}}
+ {{define "body"}}
+ <main class="main-width form-wrap">
+ {{if .Form.HasError}}
+ <div class="notice notice--error">
+ <strong>{{template "error"}}:</strong>
+ {{.Form.Error}}
+ </div>
+ {{end}}
+
+ <h2>{{block "change password" .}}Change password{{end}}</h2>
+
+ {{if eq .U.Source "local"}}
+ <form action="/settings/change-password" method="post">
+ <div class="form-field">
+ <label for="current_pass">{{block "current password" .}}Current password{{end}}</label>
+ <input required type="password" autocomplete="current-password" id="current_pass" name="current_password">
+ <br>
+ <br>
+ <label for="pass">{{block "password" .}}Password{{end}}</label>
+ <input required type="password" autocomplete="new-password" id="pass" name="password">
+ <br>
+ <br>
+ <label for="pass_confirm">{{block "confirm password" .}}Confirm password{{end}}</label>
+ <input required type="password" autocomplete="new-password" id="pass_confirm" name="password_confirm">
+ </div>
+
+ <div class="form-field">
+ <input class="btn" type="submit" value='{{block "submit" .}}Submit{{end}}'>
+ </div>
+ </form>
+ {{else}}
+ <p>{{block "non local password change" .}}Non-local accounts cannot have their passwords changed.{{end}}</p>
+ {{end}}
+ </main>
+ {{end}}
diff --git a/web/web.go b/web/web.go
index 299f9e5..3ebfc14 100644
--- a/web/web.go
+++ b/web/web.go
@@ -7,6 +7,7 @@ import (
"net/url"
"github.com/bouncepaw/mycorrhiza/admin"
+ "github.com/bouncepaw/mycorrhiza/settings"
"github.com/bouncepaw/mycorrhiza/auth"
"github.com/bouncepaw/mycorrhiza/backlinks"
"github.com/bouncepaw/mycorrhiza/categories"
@@ -67,6 +68,11 @@ func Handler() http.Handler {
adminRouter := wikiRouter.PathPrefix("/admin").Subrouter()
adminRouter.Use(groupMiddleware("admin"))
admin.Init(adminRouter)
+
+ settingsRouter := wikiRouter.PathPrefix("/settings").Subrouter()
+ // TODO: check if necessary?
+ //settingsRouter.Use(groupMiddleware("settings"))
+ settings.Init(settingsRouter)
}
// Index page
--
2.43.0
Thank you! Ran a quick test, seems to work as expected. Applying. I will provide
the translation myself.