From 59b565ae193784c51aa5126c2a62e63a363160cc Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Thu, 5 Jan 2023 13:55:05 +0100 Subject: [PATCH] Split "auth" and "user" routes --- internal/dis/component/auth/auth.go | 30 +------- .../component/auth/{templates => }/login.html | 0 internal/dis/component/auth/panel/panel.go | 76 +++++++++++++++++++ .../auth/{routes.go => panel/password.go} | 31 +++----- .../auth/{ => panel}/templates/password.html | 2 +- .../{ => panel}/templates/totp_disable.html | 2 +- .../{ => panel}/templates/totp_enable.html | 2 +- .../{ => panel}/templates/totp_enroll.html | 2 +- .../auth/{ => panel}/templates/user.html | 2 +- .../dis/component/auth/{ => panel}/totp.go | 47 ++++++------ internal/dis/component/auth/panel/user.go | 26 +++++++ internal/dis/component/auth/protect.go | 2 +- internal/dis/component/auth/session.go | 6 +- internal/dis/component/auth/user.go | 17 ----- internal/dis/distillery.go | 2 + 15 files changed, 148 insertions(+), 99 deletions(-) rename internal/dis/component/auth/{templates => }/login.html (100%) create mode 100644 internal/dis/component/auth/panel/panel.go rename internal/dis/component/auth/{routes.go => panel/password.go} (73%) rename internal/dis/component/auth/{ => panel}/templates/password.html (84%) rename internal/dis/component/auth/{ => panel}/templates/totp_disable.html (89%) rename internal/dis/component/auth/{ => panel}/templates/totp_enable.html (91%) rename internal/dis/component/auth/{ => panel}/templates/totp_enroll.html (91%) rename internal/dis/component/auth/{ => panel}/templates/user.html (96%) rename internal/dis/component/auth/{ => panel}/totp.go (77%) create mode 100644 internal/dis/component/auth/panel/user.go diff --git a/internal/dis/component/auth/auth.go b/internal/dis/component/auth/auth.go index a705d49..d77264a 100644 --- a/internal/dis/component/auth/auth.go +++ b/internal/dis/component/auth/auth.go @@ -27,13 +27,10 @@ var ( _ component.Routeable = (*Auth)(nil) ) -func (auth *Auth) Routes() []string { return []string{"/user/"} } +func (auth *Auth) Routes() []string { return []string{"/auth/"} } func (auth *Auth) HandleRoute(ctx context.Context, route string) (http.Handler, error) { router := httprouter.New() - - router.Handler(http.MethodGet, route, auth.authUser(ctx)) - { login := auth.authLogin(ctx) router.Handler(http.MethodGet, route+"login", login) @@ -42,35 +39,12 @@ func (auth *Auth) HandleRoute(ctx context.Context, route string) (http.Handler, router.Handler(http.MethodGet, route+"logout", auth.authLogout(ctx)) - { - password := auth.authPassword(ctx) - router.Handler(http.MethodGet, route+"password", password) - router.Handler(http.MethodPost, route+"password", password) - } - - { - totpenable := auth.authTOTPEnable(ctx) - router.Handler(http.MethodGet, route+"totp/enable", totpenable) - router.Handler(http.MethodPost, route+"totp/enable", totpenable) - } - - { - totpenroll := auth.authTOTPEnroll(ctx) - router.Handler(http.MethodGet, route+"totp/enroll", totpenroll) - router.Handler(http.MethodPost, route+"totp/enroll", totpenroll) - } - - { - totpdisable := auth.authTOTPDisable(ctx) - router.Handler(http.MethodGet, route+"totp/disable", totpdisable) - router.Handler(http.MethodPost, route+"totp/disable", totpdisable) - } - return router, nil } func (auth *Auth) CSRF() func(http.Handler) http.Handler { // setup the csrf handler (if needed) + // TOOD: This should move to the server handler return auth.csrf.Get(func() func(http.Handler) http.Handler { var opts []csrf.Option if !auth.Config.HTTPSEnabled() { diff --git a/internal/dis/component/auth/templates/login.html b/internal/dis/component/auth/login.html similarity index 100% rename from internal/dis/component/auth/templates/login.html rename to internal/dis/component/auth/login.html diff --git a/internal/dis/component/auth/panel/panel.go b/internal/dis/component/auth/panel/panel.go new file mode 100644 index 0000000..06191ba --- /dev/null +++ b/internal/dis/component/auth/panel/panel.go @@ -0,0 +1,76 @@ +package panel + +import ( + "context" + "net/http" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" + "github.com/FAU-CDI/wisski-distillery/internal/models" + "github.com/FAU-CDI/wisski-distillery/pkg/httpx" + "github.com/julienschmidt/httprouter" +) + +type UserPanel struct { + component.Base + Dependencies struct { + Auth *auth.Auth + } +} + +var ( + _ component.Routeable = (*UserPanel)(nil) +) + +func (panel *UserPanel) Routes() []string { return []string{"/user/"} } + +func (panel *UserPanel) HandleRoute(ctx context.Context, route string) (http.Handler, error) { + router := httprouter.New() + + { + user := panel.routeUser(ctx) + router.Handler(http.MethodGet, route, user) + } + + { + password := panel.routePassword(ctx) + router.Handler(http.MethodGet, route+"password", password) + router.Handler(http.MethodPost, route+"password", password) + } + + { + totpenable := panel.routeTOTPEnable(ctx) + router.Handler(http.MethodGet, route+"totp/enable", totpenable) + router.Handler(http.MethodPost, route+"totp/enable", totpenable) + } + + { + totpenroll := panel.routeTOTPEnroll(ctx) + router.Handler(http.MethodGet, route+"totp/enroll", totpenroll) + router.Handler(http.MethodPost, route+"totp/enroll", totpenroll) + } + + { + totpdisable := panel.routeTOTPDisable(ctx) + router.Handler(http.MethodGet, route+"totp/disable", totpdisable) + router.Handler(http.MethodPost, route+"totp/disable", totpdisable) + } + + // ensure that the user is logged in! + return panel.Dependencies.Auth.Protect(router, nil), nil +} + +type userFormContext struct { + httpx.FormContext + User *models.User +} + +func (panel *UserPanel) UserFormContext(ctx httpx.FormContext, r *http.Request) any { + user, err := panel.Dependencies.Auth.UserOf(r) + + uctx := userFormContext{FormContext: ctx} + if err == nil { + uctx.User = &user.User + } + return uctx +} diff --git a/internal/dis/component/auth/routes.go b/internal/dis/component/auth/panel/password.go similarity index 73% rename from internal/dis/component/auth/routes.go rename to internal/dis/component/auth/panel/password.go index 6f3e9e7..ced8267 100644 --- a/internal/dis/component/auth/routes.go +++ b/internal/dis/component/auth/panel/password.go @@ -1,29 +1,16 @@ -package auth +package panel import ( "context" - _ "embed" "errors" "net/http" + _ "embed" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" ) -//go:embed "templates/user.html" -var userHTMLStr string -var userTemplate = static.AssetsUser.MustParseShared( - "user.html", - userHTMLStr, -) - -func (auth *Auth) authUser(ctx context.Context) http.Handler { - return auth.Protect(&httpx.HTMLHandler[*AuthUser]{ - Handler: auth.UserOf, - Template: userTemplate, - }, nil) -} - //go:embed "templates/password.html" var passwordHTMLString string var passwordTemplate = static.AssetsUser.MustParseShared("password.html", passwordHTMLString) @@ -37,23 +24,23 @@ var ( errPasswordSet = errors.New("password was updated") ) -func (auth *Auth) authPassword(ctx context.Context) http.Handler { +func (panel *UserPanel) routePassword(ctx context.Context) http.Handler { return &httpx.Form[struct{}]{ Fields: []httpx.Field{ {Name: "old", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, - {Name: "passcode", Type: httpx.TextField, EmptyOnError: true, Label: "Current Passcode (optional)"}, + {Name: "otp", Type: httpx.TextField, EmptyOnError: true, Label: "Current Passcode (optional)"}, {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(), + CSRF: panel.Dependencies.Auth.CSRF(), RenderTemplate: passwordTemplate, - RenderTemplateContext: auth.UserFormContext, + RenderTemplateContext: panel.UserFormContext, Validate: func(r *http.Request, values map[string]string) (struct{}, error) { - old, passcode, new, new2 := values["old"], values["passcode"], values["new"], values["new2"] + old, passcode, new, new2 := values["old"], values["otp"], values["new"], values["new2"] if new != new2 { return struct{}{}, errPasswordsNotIdentical @@ -63,7 +50,7 @@ func (auth *Auth) authPassword(ctx context.Context) http.Handler { return struct{}{}, errPasswordIsEmpty } - user, err := auth.UserOf(r) + user, err := panel.Dependencies.Auth.UserOf(r) if err != nil { return struct{}{}, err } diff --git a/internal/dis/component/auth/templates/password.html b/internal/dis/component/auth/panel/templates/password.html similarity index 84% rename from internal/dis/component/auth/templates/password.html rename to internal/dis/component/auth/panel/templates/password.html index 228e897..600ce43 100644 --- a/internal/dis/component/auth/templates/password.html +++ b/internal/dis/component/auth/panel/templates/password.html @@ -8,6 +8,6 @@ Change Password

- Logout + Logout

{{ end }} diff --git a/internal/dis/component/auth/templates/totp_disable.html b/internal/dis/component/auth/panel/templates/totp_disable.html similarity index 89% rename from internal/dis/component/auth/templates/totp_disable.html rename to internal/dis/component/auth/panel/templates/totp_disable.html index 7fc4399..c422de6 100644 --- a/internal/dis/component/auth/templates/totp_disable.html +++ b/internal/dis/component/auth/panel/templates/totp_disable.html @@ -8,7 +8,7 @@ Disable TOTP

- Logout + Logout

{{ end }} diff --git a/internal/dis/component/auth/templates/totp_enable.html b/internal/dis/component/auth/panel/templates/totp_enable.html similarity index 91% rename from internal/dis/component/auth/templates/totp_enable.html rename to internal/dis/component/auth/panel/templates/totp_enable.html index bc8f881..63e5fef 100644 --- a/internal/dis/component/auth/templates/totp_enable.html +++ b/internal/dis/component/auth/panel/templates/totp_enable.html @@ -7,7 +7,7 @@ Enable TOTP

- Logout + Logout

{{ end }} {{ define "form/inside" }} diff --git a/internal/dis/component/auth/templates/totp_enroll.html b/internal/dis/component/auth/panel/templates/totp_enroll.html similarity index 91% rename from internal/dis/component/auth/templates/totp_enroll.html rename to internal/dis/component/auth/panel/templates/totp_enroll.html index 9cd4723..bb1881f 100644 --- a/internal/dis/component/auth/templates/totp_enroll.html +++ b/internal/dis/component/auth/panel/templates/totp_enroll.html @@ -7,7 +7,7 @@ Enroll TOTP

- Logout + Logout

{{ end }} {{ define "form/inside" }} diff --git a/internal/dis/component/auth/templates/user.html b/internal/dis/component/auth/panel/templates/user.html similarity index 96% rename from internal/dis/component/auth/templates/user.html rename to internal/dis/component/auth/panel/templates/user.html index 4792176..6ec9a43 100644 --- a/internal/dis/component/auth/templates/user.html +++ b/internal/dis/component/auth/panel/templates/user.html @@ -6,7 +6,7 @@ {{ .User.User }}

- Logout + Logout

{{ end }} diff --git a/internal/dis/component/auth/totp.go b/internal/dis/component/auth/panel/totp.go similarity index 77% rename from internal/dis/component/auth/totp.go rename to internal/dis/component/auth/panel/totp.go index 285ded7..e0e22cd 100644 --- a/internal/dis/component/auth/totp.go +++ b/internal/dis/component/auth/panel/totp.go @@ -1,10 +1,11 @@ -package auth +package panel import ( "context" "html/template" "net/http" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" "github.com/FAU-CDI/wisski-distillery/pkg/httpx" @@ -15,27 +16,27 @@ import ( var totpEnableStr string var totpEnableTemplate = static.AssetsUser.MustParseShared("totp_enable.html", totpEnableStr) -func (auth *Auth) authTOTPEnable(ctx context.Context) http.Handler { +func (panel *UserPanel) routeTOTPEnable(ctx context.Context) http.Handler { return &httpx.Form[struct{}]{ Fields: []httpx.Field{ {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, }, FieldTemplate: httpx.PureCSSFieldTemplate, - CSRF: auth.CSRF(), + CSRF: panel.Dependencies.Auth.CSRF(), SkipForm: func(r *http.Request) (data struct{}, skip bool) { - user, err := auth.UserOf(r) + user, err := panel.Dependencies.Auth.UserOf(r) return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled() }, RenderTemplate: totpEnableTemplate, - RenderTemplateContext: auth.UserFormContext, + RenderTemplateContext: panel.UserFormContext, Validate: func(r *http.Request, values map[string]string) (struct{}, error) { password := values["password"] - user, err := auth.UserOf(r) + user, err := panel.Dependencies.Auth.UserOf(r) if err != nil { return struct{}{}, err } @@ -73,22 +74,22 @@ type totpEnrollContext struct { TOTPURL template.URL } -func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler { +func (panel *UserPanel) routeTOTPEnroll(ctx context.Context) http.Handler { return &httpx.Form[struct{}]{ Fields: []httpx.Field{ {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, - {Name: "passcode", Type: httpx.TextField, EmptyOnError: true, Label: "Passcode"}, + {Name: "otp", Type: httpx.TextField, EmptyOnError: true, Label: "Passcode"}, }, FieldTemplate: httpx.PureCSSFieldTemplate, - CSRF: auth.CSRF(), + CSRF: panel.Dependencies.Auth.CSRF(), SkipForm: func(r *http.Request) (data struct{}, skip bool) { - user, err := auth.UserOf(r) + user, err := panel.Dependencies.Auth.UserOf(r) return struct{}{}, err == nil && user != nil && user.IsTOTPEnabled() }, RenderForm: func(context httpx.FormContext, w http.ResponseWriter, r *http.Request) { - user, err := auth.UserOf(r) + user, err := panel.Dependencies.Auth.UserOf(r) ctx := totpEnrollContext{ userFormContext: userFormContext{ @@ -100,7 +101,7 @@ func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler { ctx.userFormContext.User = &user.User secret, err := user.TOTP() if err == nil { - img, _ := TOTPLink(secret, 500, 500) + img, _ := auth.TOTPLink(secret, 500, 500) ctx.TOTPImage = template.URL(img) ctx.TOTPURL = template.URL(secret.URL()) @@ -110,9 +111,9 @@ func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler { }, Validate: func(r *http.Request, values map[string]string) (struct{}, error) { - password, passcode := values["password"], values["passcode"] + password, otp := values["password"], values["otp"] - user, err := auth.UserOf(r) + user, err := panel.Dependencies.Auth.UserOf(r) if err != nil { return struct{}{}, err } @@ -124,7 +125,7 @@ func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler { } } { - err := user.EnableTOTP(r.Context(), passcode) + err := user.EnableTOTP(r.Context(), otp) if err != nil { return struct{}{}, errTOTPSetFailure } @@ -144,33 +145,33 @@ func (auth *Auth) authTOTPEnroll(ctx context.Context) http.Handler { var totpDisableStr string var totpDisableTemplate = static.AssetsUser.MustParseShared("totp_disable.html", totpDisableStr) -func (auth *Auth) authTOTPDisable(ctx context.Context) http.Handler { +func (panel *UserPanel) routeTOTPDisable(ctx context.Context) http.Handler { return &httpx.Form[struct{}]{ Fields: []httpx.Field{ {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Current Password"}, - {Name: "passcode", Type: httpx.TextField, EmptyOnError: true, Label: "Current Passcode"}, + {Name: "otp", Type: httpx.TextField, EmptyOnError: true, Label: "Current Passcode"}, }, FieldTemplate: httpx.PureCSSFieldTemplate, - CSRF: auth.CSRF(), + CSRF: panel.Dependencies.Auth.CSRF(), SkipForm: func(r *http.Request) (data struct{}, skip bool) { - user, err := auth.UserOf(r) + user, err := panel.Dependencies.Auth.UserOf(r) return struct{}{}, err == nil && user != nil && !user.IsTOTPEnabled() }, RenderTemplate: totpDisableTemplate, - RenderTemplateContext: auth.UserFormContext, + RenderTemplateContext: panel.UserFormContext, Validate: func(r *http.Request, values map[string]string) (struct{}, error) { - password, passcode := values["password"], values["passcode"] + password, otp := values["password"], values["otp"] - user, err := auth.UserOf(r) + user, err := panel.Dependencies.Auth.UserOf(r) if err != nil { return struct{}{}, err } { - err := user.CheckCredentials(r.Context(), []byte(password), passcode) + err := user.CheckCredentials(r.Context(), []byte(password), otp) if err != nil { return struct{}{}, errCredentialsIncorrect } diff --git a/internal/dis/component/auth/panel/user.go b/internal/dis/component/auth/panel/user.go new file mode 100644 index 0000000..6b31e35 --- /dev/null +++ b/internal/dis/component/auth/panel/user.go @@ -0,0 +1,26 @@ +package panel + +import ( + "context" + "net/http" + + _ "embed" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static" + "github.com/FAU-CDI/wisski-distillery/pkg/httpx" +) + +//go:embed "templates/user.html" +var userHTMLStr string +var userTemplate = static.AssetsUser.MustParseShared( + "user.html", + userHTMLStr, +) + +func (panel *UserPanel) routeUser(ctx context.Context) http.Handler { + return &httpx.HTMLHandler[*auth.AuthUser]{ + Handler: panel.Dependencies.Auth.UserOf, + Template: userTemplate, + } +} diff --git a/internal/dis/component/auth/protect.go b/internal/dis/component/auth/protect.go index 37b1cc1..ed399af 100644 --- a/internal/dis/component/auth/protect.go +++ b/internal/dis/component/auth/protect.go @@ -33,7 +33,7 @@ func (auth *Auth) Protect(handler http.Handler, perm Permission) http.Handler { } // redirect the user to the login endpoint, with the original URI as a return - dest := "/user/login?next=" + url.QueryEscape(r.URL.RequestURI()) + dest := "/auth/login?next=" + url.QueryEscape(r.URL.RequestURI()) http.Redirect(w, r, dest, http.StatusSeeOther) return } diff --git a/internal/dis/component/auth/session.go b/internal/dis/component/auth/session.go index cb23ff7..2339bcf 100644 --- a/internal/dis/component/auth/session.go +++ b/internal/dis/component/auth/session.go @@ -101,7 +101,7 @@ func (auth *Auth) Logout(w http.ResponseWriter, r *http.Request) error { return sess.Save(r, w) } -//go:embed "templates/login.html" +//go:embed "login.html" var loginHTMLStr string var loginTemplate = static.AssetsUser.MustParseShared("login.html", loginHTMLStr) @@ -118,7 +118,7 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler { Fields: []httpx.Field{ {Name: "username", Type: httpx.TextField, Label: "Username"}, {Name: "password", Type: httpx.PasswordField, EmptyOnError: true, Label: "Password"}, - {Name: "passcode", Type: httpx.TextField, EmptyOnError: true, Label: "Passcode (optional)"}, + {Name: "otp", Type: httpx.TextField, EmptyOnError: true, Label: "Passcode (optional)"}, }, FieldTemplate: httpx.PureCSSFieldTemplate, @@ -132,7 +132,7 @@ func (auth *Auth) authLogin(ctx context.Context) http.Handler { }, Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) { - username, password, passcode := values["username"], values["password"], values["passcode"] + username, password, passcode := values["username"], values["password"], values["otp"] // make sure that the user exists user, err := auth.User(ctx, username) diff --git a/internal/dis/component/auth/user.go b/internal/dis/component/auth/user.go index 314faae..53de3fc 100644 --- a/internal/dis/component/auth/user.go +++ b/internal/dis/component/auth/user.go @@ -6,10 +6,8 @@ import ( "encoding/base64" "fmt" "image/png" - "net/http" "github.com/FAU-CDI/wisski-distillery/internal/models" - "github.com/FAU-CDI/wisski-distillery/pkg/httpx" "github.com/pkg/errors" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" @@ -292,18 +290,3 @@ func (au *AuthUser) Delete(ctx context.Context) error { return table.Delete(&au.User).Error } - -type userFormContext struct { - httpx.FormContext - User *models.User -} - -func (au *Auth) UserFormContext(ctx httpx.FormContext, r *http.Request) any { - user, err := au.UserOf(r) - - uctx := userFormContext{FormContext: ctx} - if err == nil { - uctx.User = &user.User - } - return uctx -} diff --git a/internal/dis/distillery.go b/internal/dis/distillery.go index ddc5bf0..b66d75f 100644 --- a/internal/dis/distillery.go +++ b/internal/dis/distillery.go @@ -8,6 +8,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/panel" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/policy" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/admin" @@ -132,6 +133,7 @@ func (dis *Distillery) allComponents() []initFunc { // auth auto[*auth.Auth], auto[*policy.Policy], + auto[*panel.UserPanel], // instances auto[*instances.Instances],