Split "auth" and "user" routes
This commit is contained in:
parent
f3939c5016
commit
59b565ae19
15 changed files with 148 additions and 99 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
76
internal/dis/component/auth/panel/panel.go
Normal file
76
internal/dis/component/auth/panel/panel.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 }}
|
||||
|
|
@ -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 }}
|
||||
|
||||
|
|
@ -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" }}
|
||||
|
|
@ -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" }}
|
||||
|
|
@ -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 }}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
26
internal/dis/component/auth/panel/user.go
Normal file
26
internal/dis/component/auth/panel/user.go
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue