diff --git a/internal/dis/component/auth/auth.go b/internal/dis/component/auth/auth.go index 6af2636..ce50b40 100644 --- a/internal/dis/component/auth/auth.go +++ b/internal/dis/component/auth/auth.go @@ -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 } diff --git a/internal/dis/component/auth/home.go b/internal/dis/component/auth/home.go index afbd154..ef2a7a5 100644 --- a/internal/dis/component/auth/home.go +++ b/internal/dis/component/auth/home.go @@ -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 + }, + } +} diff --git a/internal/dis/component/auth/session.go b/internal/dis/component/auth/session.go index bfa4208..9395b1f 100644 --- a/internal/dis/component/auth/session.go +++ b/internal/dis/component/auth/session.go @@ -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) + }) } diff --git a/internal/dis/component/auth/templates/home.html b/internal/dis/component/auth/templates/home.html index ef5c94c..cd77003 100644 --- a/internal/dis/component/auth/templates/home.html +++ b/internal/dis/component/auth/templates/home.html @@ -10,20 +10,23 @@ {{ define "content" }}