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],