Password change

This commit is contained in:
Sung Won Cho 2022-04-17 16:52:55 +10:00
commit 0a40fef084
13 changed files with 138 additions and 25 deletions

View file

@ -51,4 +51,10 @@ var (
// ErrPasswordResetTokenExpired is an error for expired password reset token
ErrPasswordResetTokenExpired appError = "this link has been expired. Please request a new password reset link."
// ErrInvalidPasswordChangeInput is an error for changing password
ErrInvalidPasswordChangeInput appError = "Both current and new passwords are required to change the password."
// ErrInvalidCurrentPassword is an error for invalid current password while changing password
ErrInvalidCurrentPassword appError = "Current password does not match."
)

View file

@ -34,8 +34,7 @@
.alert {
position: relative;
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
padding: 1.75rem 1.25rem;
border: 1px solid transparent;
}
@ -48,7 +47,7 @@
}
.alert-dismissible {
padding-right: 4rem;
// padding-right: 4rem;
}
.alert-dismissible .close {
@ -170,3 +169,8 @@
.alert-dark .alert-link {
color: #040505;
}
// custom
.alert-slim {
padding: 0.75rem 1.25rem;
}

View file

@ -81,4 +81,8 @@
margin-top: rem(16px);
}
}
.alert {
margin-bottom: 1rem;
}
}

View file

@ -183,9 +183,10 @@ type SessionResponse struct {
func logError(err error, msg string) {
// log if internal error
if _, ok := err.(views.PublicError); !ok {
log.ErrorWrap(err, msg)
}
// if _, ok := err.(views.PublicError); !ok {
// log.ErrorWrap(err, msg)
// }
log.ErrorWrap(err, msg)
}
func getStatusCode(err error) int {
@ -214,6 +215,10 @@ func getStatusCode(err error) int {
return http.StatusGone
case app.ErrPasswordConfirmationMismatch:
return http.StatusBadRequest
case app.ErrInvalidPasswordChangeInput:
return http.StatusBadRequest
case app.ErrInvalidCurrentPassword:
return http.StatusBadRequest
}
return http.StatusInternalServerError

View file

@ -37,8 +37,8 @@ func NewWebRoutes(a *app.App, c *Controllers) []Route {
{"GET", "/password-reset", c.Users.PasswordResetView.ServeHTTP, true},
{"PATCH", "/password-reset", c.Users.PasswordReset, true},
{"PATCH", "/account/profile", c.Users.PasswordReset, true},
{"PATCH", "/account/password", c.Users.PasswordReset, true},
{"PATCH", "/account/profile", mw.Auth(a, c.Users.PasswordReset, nil), true},
{"PATCH", "/account/password", mw.Auth(a, c.Users.PasswordUpdate, nil), true},
{"GET", "/password-reset/{token}", c.Users.PasswordResetConfirm, true},
{"POST", "/reset-token", c.Users.CreateResetToken, true},
}

View file

@ -1,12 +1,14 @@
package controllers
import (
"fmt"
"net/http"
"net/url"
"time"
"github.com/dnote/dnote/pkg/server/app"
"github.com/dnote/dnote/pkg/server/buildinfo"
"github.com/dnote/dnote/pkg/server/context"
"github.com/dnote/dnote/pkg/server/database"
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/log"
@ -51,11 +53,11 @@ func NewUsers(app *app.App) *Users {
"users/password_reset_confirm",
),
SettingView: views.NewView(app,
views.Config{Layout: "base", HelperFuncs: commonHelpers, AlertInBody: true, HeaderTemplate: "navbar"},
views.Config{Layout: "base", HelperFuncs: commonHelpers, HeaderTemplate: "navbar"},
"users/settings",
),
AboutView: views.NewView(app,
views.Config{Title: "About", Layout: "base", HelperFuncs: commonHelpers, AlertInBody: true, HeaderTemplate: "navbar"},
views.Config{Title: "About", Layout: "base", HelperFuncs: commonHelpers, HeaderTemplate: "navbar"},
"users/settings_about",
),
app: app,
@ -447,3 +449,81 @@ func (u *Users) About(w http.ResponseWriter, r *http.Request) {
u.AboutView.Render(w, r, &vd, http.StatusOK)
}
type updatePasswordForm struct {
OldPassword string `schema:"old_password"`
NewPassword string `schema:"new_password"`
NewPasswordConfirmation string `schema:"new_password_confirmation"`
}
func (u *Users) PasswordUpdate(w http.ResponseWriter, r *http.Request) {
vd := views.Data{}
user := context.User(r.Context())
if user == nil {
handleHTMLError(w, r, app.ErrLoginRequired, "No authenticated user found", u.SettingView, vd)
return
}
var form updatePasswordForm
if err := parseRequestData(r, &form); err != nil {
handleHTMLError(w, r, err, "parsing payload", u.LoginView, vd)
return
}
fmt.Println(form)
if form.OldPassword == "" || form.NewPassword == "" {
handleHTMLError(w, r, app.ErrInvalidPasswordChangeInput, "invalid params", u.SettingView, vd)
return
}
if form.NewPassword != form.NewPasswordConfirmation {
handleHTMLError(w, r, app.ErrPasswordConfirmationMismatch, "passwords do not match", u.SettingView, vd)
return
}
var account database.Account
if err := u.app.DB.Where("user_id = ?", user.ID).First(&account).Error; err != nil {
handleHTMLError(w, r, err, "getting account", u.SettingView, vd)
return
}
password := []byte(form.OldPassword)
if err := bcrypt.CompareHashAndPassword([]byte(account.Password.String), password); err != nil {
log.WithFields(log.Fields{
"user_id": user.ID,
}).Warn("invalid password update attempt")
handleHTMLError(w, r, app.ErrInvalidCurrentPassword, "invalid password", u.SettingView, vd)
return
}
if err := validatePassword(form.NewPassword); err != nil {
handleHTMLError(w, r, err, "invalid password", u.SettingView, vd)
return
}
hashedNewPassword, err := bcrypt.GenerateFromPassword([]byte(form.NewPassword), bcrypt.DefaultCost)
if err != nil {
handleHTMLError(w, r, err, "hashing password", u.SettingView, vd)
return
}
if err := u.app.DB.Model(&account).Update("password", string(hashedNewPassword)).Error; err != nil {
handleHTMLError(w, r, err, "updating password", u.SettingView, vd)
return
}
alert := views.Alert{
Level: views.AlertLvlSuccess,
Message: "Password change successful",
}
views.RedirectAlert(w, r, "/", http.StatusFound, alert)
}
func validatePassword(password string) error {
if len(password) < 8 {
return app.ErrPasswordTooShort
}
return nil
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,6 @@
package views
import (
// "html/template"
"net/http"
"time"
@ -92,12 +91,14 @@ func persistAlert(w http.ResponseWriter, alert Alert) {
Name: "alert_level",
Value: alert.Level,
Expires: expiresAt,
Path: "/",
HttpOnly: true,
}
msg := http.Cookie{
Name: "alert_message",
Value: alert.Message,
Expires: expiresAt,
Path: "/",
HttpOnly: true,
}
http.SetCookie(w, &lvl)

View file

@ -1,8 +1,9 @@
{{define "alert"}}
{{if .}}
<div class="alert alert-{{.Level}} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{{.Message}}
<div class="alert alert-{{.Level}}" role="alert">
<div class="container">
{{.Message}}
</div>
</div>
{{end}}
{{end}}

View file

@ -9,14 +9,14 @@
<div class="body">
{{if .Referrer}}
<div class="alert alert-info" role="alert">
<div class="alert alert-info alert-slim" role="alert">
Please sign in to continue to that page.
</div>
{{end}}
<div class="panel">
{{if .Alert}}
<div class="alert alert-{{.Alert.Level}}" role="alert">
<div class="alert alert-{{.Alert.Level}} alert-slim" role="alert">
{{.Alert.Message}}
</div>
{{end}}

View file

@ -18,11 +18,11 @@
<div class="setting-row">
<div class="setting-row-summary">
<div>
<h3 class="setting-name">Email</h3>
<h3 class="setting-name">Current Email</h3>
</div>
<div class="setting-right">
{{.Account.Email.String}}
{{.Email}}
</div>
</div>
</div>
@ -34,7 +34,7 @@
</div>
<div class="setting-right">
{{if .Account.EmailVerified}}
{{if .EmailVerified}}
Yes
{{else}}
No
@ -125,8 +125,9 @@
{{end}}
{{define "passwordChangeForm"}}
<form action="/account/password">
<input type="password" style="display: none;" readOnly />
<form action="/account/password" method="POST">
<input type="password" style="display: none;" readonly />
<input type="hidden" name="_method" value="PATCH" />
<div class="input-row">
<label class="input-label" for="old-password-input">
@ -134,6 +135,7 @@
</label>
<input
id="old-password-input"
name="old_password"
type="password"
placeholder="********"
class="form-control"
@ -147,6 +149,7 @@
</label>
<input
id="new-password-input"
name="new_password"
type="password"
placeholder="********"
class="form-control"
@ -162,6 +165,7 @@
</label>
<input
id="new-password-confirmation-input"
name="new_password_confirmation"
type="password"
placeholder="********"
class="form-control"

View file

@ -128,11 +128,19 @@ func (v *View) Render(w http.ResponseWriter, r *http.Request, data *Data, status
if vd.Yield == nil {
vd.Yield = map[string]interface{}{}
}
vd.Yield["Account"] = &vd.Account
vd.Yield["User"] = &vd.User
if vd.Account != nil {
vd.Yield["Email"] = &vd.Account.Email.String
vd.Yield["EmailVerified"] = &vd.Account.EmailVerified
}
if vd.User != nil {
vd.Yield["Cloud"] = &vd.User.Cloud
}
vd.Yield["CurrentPath"] = r.URL.Path
vd.Yield["Standalone"] = buildinfo.Standalone
fmt.Println("######")
fmt.Println(vd.Alert)
var buf bytes.Buffer
csrfField := csrf.TemplateField(r)
tpl := v.Template.Funcs(template.FuncMap{