Add change password feature

This commit is contained in:
Tom Wiesing 2022-12-28 17:58:29 +01:00
parent 515142c055
commit b9795be745
No known key found for this signature in database
10 changed files with 228 additions and 96 deletions

View file

@ -43,12 +43,19 @@ func (auth *Auth) HandleRoute(ctx context.Context, route string) (http.Handler,
router.Handler(http.MethodGet, route, auth.authHome(ctx))
login := auth.loginForm()
{
login := auth.authLogin(ctx)
router.Handler(http.MethodGet, route+"login", login)
router.Handler(http.MethodPost, route+"login", login)
}
router.Handler(http.MethodGet, route+"login", login)
router.Handler(http.MethodPost, route+"login", login)
router.Handler(http.MethodGet, route+"logout", auth.authLogout(ctx))
router.HandlerFunc(http.MethodGet, route+"logout", auth.logoutRoute)
{
password := auth.authPassword(ctx)
router.Handler(http.MethodGet, route+"password", password)
router.Handler(http.MethodPost, route+"password", password)
}
return router, nil
}

View file

@ -3,6 +3,8 @@ package auth
import (
"context"
_ "embed"
"errors"
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
@ -22,3 +24,80 @@ func (auth *Auth) authHome(ctx context.Context) http.Handler {
Template: homeTemplate,
}, nil)
}
//go:embed "templates/password.html"
var passwordHTMLString string
var passwordTemplate = static.AssetsAuthLogin.MustParseShared("password.html", passwordHTMLString)
type authpasswordContext struct {
Message string
Form template.HTML
}
var (
errPasswordsNotIdentical = errors.New("passwords are not identical")
errPasswordIsEmpty = errors.New("password is empty")
errPasswordIncorrect = errors.New("old password is not correct")
errPasswordSetFailure = errors.New("error saving new password")
errPasswordSet = errors.New("password was updated")
)
func (auth *Auth) authPassword(ctx context.Context) http.Handler {
return &httpx.Form[struct{}]{
Fields: []httpx.Field{
{Name: "old", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"},
{Name: "new", Type: httpx.PasswordField, EmptyOnError: true, Label: "New Password"},
{Name: "new2", Type: httpx.PasswordField, EmptyOnError: true, Label: "New Password (again)"},
},
FieldTemplate: httpx.PureCSSFieldTemplate,
CSRF: auth.csrf.Get(nil),
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) {
ctx := authpasswordContext{
Message: "",
Form: template,
}
if err != nil {
ctx.Message = err.Error()
}
httpx.WriteHTML(ctx, nil, passwordTemplate, "", w, r)
},
Validate: func(r *http.Request, values map[string]string) (struct{}, error) {
old, new, new2 := values["old"], values["new"], values["new2"]
if new != new2 {
return struct{}{}, errPasswordsNotIdentical
}
if new == "" {
return struct{}{}, errPasswordIsEmpty
}
user, err := auth.UserOf(r)
if err != nil {
return struct{}{}, err
}
{
err := user.CheckPassword(r.Context(), []byte(old))
if err != nil {
return struct{}{}, errPasswordIncorrect
}
}
{
err := user.SetPassword(r.Context(), []byte(new))
if err != nil {
return struct{}{}, errPasswordSetFailure
}
}
return struct{}{}, nil
},
RenderSuccess: func(_ struct{}, values map[string]string, w http.ResponseWriter, r *http.Request) error {
return errPasswordSet
},
}
}

View file

@ -13,22 +13,6 @@ import (
_ "embed"
)
type contextUserKey struct{}
var ctxUserKey = contextUserKey{}
const (
sessionCookieName = "distillery-session"
sessionUserKey = "user"
)
// session returns the session belonging to a request
func (auth *Auth) session(r *http.Request) (*sessions.Session, error) {
return auth.store.Get(func() sessions.Store {
return sessions.NewCookieStore([]byte(auth.Config.SessionSecret))
}).Get(r, sessionCookieName)
}
// UserOf returns the user logged into the given request.
// If there is no user associated with the given user, user and error will be nil.
//
@ -74,8 +58,29 @@ func (auth *Auth) UserOf(r *http.Request) (user *AuthUser, err error) {
return user, nil
}
// writeLogin marks the user as logged in on the given writer
func (auth *Auth) writeLogin(w http.ResponseWriter, r *http.Request, user *AuthUser) error {
const sessionCookieName = "distillery-session"
// session returns the session that belongs to a given request.
// If the session is not set, creates a new session.
func (auth *Auth) session(r *http.Request) (*sessions.Session, error) {
return auth.store.Get(func() sessions.Store {
return sessions.NewCookieStore([]byte(auth.Config.SessionSecret))
}).Get(r, sessionCookieName)
}
const sessionUserKey = "user"
type contextUserKey struct{}
var ctxUserKey = contextUserKey{}
// Login logs a user into the given request.
//
// If a user was previously logged into this session,
// UserOf may not return the correct user until the user makes a new request.
//
// It is recommended to send a HTTP redirect to make sure a new request is made.
func (auth *Auth) Login(w http.ResponseWriter, r *http.Request, user *AuthUser) error {
sess, err := auth.session(r)
if err != nil {
return err
@ -84,8 +89,11 @@ func (auth *Auth) writeLogin(w http.ResponseWriter, r *http.Request, user *AuthU
return sess.Save(r, w)
}
// writeLogout logs out the user form the given session
func (auth *Auth) writeLogout(w http.ResponseWriter, r *http.Request) error {
// Logout logs out the user from the given session.
//
// UserOf may return incorrect results until the user makes a new request.
// It is recommended to send a HTTP redirect to make sure a new request is made.
func (auth *Auth) Logout(w http.ResponseWriter, r *http.Request) error {
sess, err := auth.session(r)
if err != nil {
return err
@ -103,7 +111,7 @@ var loginResponse = httpx.Response{
Body: []byte("user is signed in"),
}
type loginContext struct {
type authloginContext struct {
Message string
Form template.HTML
}
@ -160,30 +168,29 @@ func (auth *Auth) Protect(handler http.Handler, perm func(user *AuthUser, r *htt
})
}
// loginForm returns the login form handler.
// auth.csrf must have been populated
func (auth *Auth) loginForm() *httpx.Form[*AuthUser] {
// authLogin implements a view to login a user
func (auth *Auth) authLogin(ctx context.Context) http.Handler {
return &httpx.Form[*AuthUser]{
Fields: []httpx.Field{
{Name: "username", Type: httpx.TextField},
{Name: "password", Type: httpx.PasswordField},
{Name: "username", Type: httpx.TextField, Label: "Username"},
{Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Password"},
},
FieldTemplate: httpx.PureCSSFieldTemplate,
CSRF: auth.csrf.Get(nil),
RenderForm: func(template template.HTML, err error, w http.ResponseWriter, r *http.Request) {
ctx := loginContext{
ctx := authloginContext{
Message: "",
Form: template,
}
if err != nil {
ctx.Message = "Login Failed"
}
httpx.WriteHTML(ctx, nil, loginTemplate, "", w, r)
},
Validate: func(ctx context.Context, values map[string]string) (*AuthUser, error) {
Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) {
username, password := values["username"], values["password"]
// make sure that the user exists
@ -206,7 +213,7 @@ func (auth *Auth) loginForm() *httpx.Form[*AuthUser] {
},
RenderSuccess: func(user *AuthUser, _ map[string]string, w http.ResponseWriter, r *http.Request) error {
if err := auth.writeLogin(w, r, user); err != nil {
if err := auth.Login(w, r, user); err != nil {
return err
}
@ -224,17 +231,19 @@ func (auth *Auth) loginForm() *httpx.Form[*AuthUser] {
}
}
func (auth *Auth) logoutRoute(w http.ResponseWriter, r *http.Request) {
// do the logout
auth.writeLogout(w, r)
// authLogout implements the authLogout view to logout a user
func (auth *Auth) authLogout(ctx context.Context) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do the logout
auth.Logout(w, r)
// get the destination
next := r.URL.Query().Get("next")
if next == "" || next[0] != '/' {
next = "/"
}
// and redirect to it!
http.Redirect(w, r, next, http.StatusSeeOther)
// get the destination
next := r.URL.Query().Get("next")
if next == "" || next[0] != '/' {
next = "/"
}
// and redirect to it!
http.Redirect(w, r, next, http.StatusSeeOther)
})
}

View file

@ -10,20 +10,23 @@
{{ define "content" }}
<div class="pure-u-1">
<div>
Welcome {{ .User.User }}!
<hr />
</div>
<form method="GET" action="/auth/password/">
<input type="submit" value="Change Password"></input>
</form>
<!-- TODO: A logout button only for now -->
<form method="GET" action="/auth/logout/">
<input type="submit" value="Logout"></input>
</form>
</div>
{{ end }}
<div class="pure-u-1">
<div class="pure-button-group" role="group" role="Actions">
<a class="pure-button" href="/auth/logout/">Logout</a>
</div>
<hr />
</div>
<div class="pure-u-1">
<div class="pure-button-group" role="group" role="Actions">
<a class="pure-button" href="/auth/password/">Change Password</a>
</div>
</div>
{{ end }}

View file

@ -1,29 +1,3 @@
{{ template "_base.html" . }}
{{ define "title" }}Login{{ end }}
{{ define "header/time" }}
<!-- no header/time -->
{{ end }}
{{ define "header"}}
<!-- no header -->
{{ end }}
{{ define "content" }}
<div class="pure-u-1">
{{ if .Message }}
<div>
{{ .Message }}
</div>
{{ end }}
<form class="pure-form" method="POST">
<fieldset>
<legend>Login Required</legend>
{{ .Form }}
<input type="submit" value="Login">
</fieldset>
</form>
</div>
{{ end }}
{{ template "_form.html" . }}
{{ define "form/title" }}Login Required{{ end }}
{{ define "form/button" }}Login{{ end }}

View file

@ -0,0 +1,9 @@
{{ template "_form.html" . }}
{{ define "form/title" }}Change Password{{ end }}
{{ define "form/button" }}Update{{ end }}
{{ define "form/extra" }}
<div>
<a class="pure-button" href="/auth/">Back</a>
<hr />
</div>
{{ end }}

View file

@ -104,7 +104,10 @@ type AuthUser struct {
models.User
}
func (au AuthUser) String() string {
func (au *AuthUser) String() string {
if au == nil {
return "User{nil}"
}
hasPassword := len(au.PasswordHash) > 0
return fmt.Sprintf("User{Name:%q,Enabled:%t,HasPassword:%t,Admin:%t}", au.User.User, au.User.Enabled, hasPassword, au.User.Admin)
}
@ -126,12 +129,16 @@ func (au *AuthUser) UnsetPassword(ctx context.Context) error {
return au.Save(ctx)
}
var ErrNoUser = errors.New("user is nil")
var ErrUserDisabled = errors.New("user is disabled")
var ErrUserBlank = errors.New("user has no password set")
// CheckPassword checks if this user can login with the provided password.
// Returns nil on success, an error otherwise.
func (au *AuthUser) CheckPassword(ctx context.Context, password []byte) error {
if au == nil {
return ErrNoUser
}
if !au.User.Enabled {
return ErrUserDisabled
}