Split "auth" and "user" routes

This commit is contained in:
Tom Wiesing 2023-01-05 13:55:05 +01:00
parent f3939c5016
commit 59b565ae19
No known key found for this signature in database
15 changed files with 148 additions and 99 deletions

View file

@ -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() {

View file

@ -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
}

View file

@ -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
}

View file

@ -8,6 +8,6 @@
<a class="pure-button pure-button-primary" href="/user/password/">Change Password</a>
</p>
<p>
<a class="pure-button pure-button-small" href="/user/logout/">Logout</a>
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
</p>
{{ end }}

View file

@ -8,7 +8,7 @@
<a class="pure-button pure-button-primary" href="/user/totp/disable/">Disable TOTP</a>
</p>
<p>
<a class="pure-button pure-button-small" href="/user/logout/">Logout</a>
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
</p>
{{ end }}

View file

@ -7,7 +7,7 @@
<a class="pure-button pure-button-primary" href="/user/totp/enable/">Enable TOTP</a>
</p>
<p>
<a class="pure-button pure-button-small" href="/user/logout/">Logout</a>
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
</p>
{{ end }}
{{ define "form/inside" }}

View file

@ -7,7 +7,7 @@
<a class="pure-button pure-button-primary" href="/user/totp/enroll/">Enroll TOTP</a>
</p>
<p>
<a class="pure-button pure-button-small" href="/user/logout/">Logout</a>
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
</p>
{{ end }}
{{ define "form/inside" }}

View file

@ -6,7 +6,7 @@
<a class="pure-button pure-button-primary" href="/user/">{{ .User.User }}</a>
</p>
<p>
<a class="pure-button pure-button-small" href="/user/logout/">Logout</a>
<a class="pure-button pure-button-small" href="/auth/logout/">Logout</a>
</p>
{{ end }}

View file

@ -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
}

View file

@ -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,
}
}

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

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