Skip to content

Commit

Permalink
feat: add json & form handlers for password and passwordreset modules…
Browse files Browse the repository at this point in the history
…; drop shieldtempl; use nix for CI
  • Loading branch information
roman-vanesyan committed Dec 29, 2024
1 parent f9b482e commit 6a5670c
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 191 deletions.
28 changes: 8 additions & 20 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ on:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ["1.22.x"]
permissions:
id-token: "write"
contents: "read"

services:
postgres:
Expand All @@ -29,20 +29,8 @@ jobs:

steps:
- uses: actions/checkout@v4
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}

- name: Restore Cache
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-v1-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run Tests
run: go test -v ./...
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- uses: DeterminateSystems/flake-checker-action@main
- name: Test
run: nix develop --command go test -count=1 -race -v ./...
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module go.inout.gg/shield
go 1.23.1

require (
github.com/a-h/templ v0.2.778
github.com/coreos/go-oidc/v3 v3.11.0
github.com/go-playground/mold/v4 v4.5.0
github.com/go-playground/validator/v10 v10.22.1
Expand All @@ -30,6 +29,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-webauthn/x v0.1.14 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-tpm v0.9.1 // indirect
github.com/gosimple/slug v1.13.1 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/caarlos0/env/v11 v11.1.0 h1:a5qZqieE9ZfzdvbbdhTalRrHT5vu/4V1/ad1Ka6frhI=
github.com/caarlos0/env/v11 v11.1.0/go.mod h1:LwgkYk1kDvfGpHthrWWLof3Ny7PezzFwS4QrsJdHTMo=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
Expand Down
1 change: 0 additions & 1 deletion shieldpassword/.test.env

This file was deleted.

121 changes: 56 additions & 65 deletions shieldpassword/form_handler.go → shieldpassword/http_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,38 @@ var (
)

const (
DefaultFieldNameFirstName = "first_name"
DefaultFieldNameLastName = "last_name"
DefaultFieldNameEmail = "email"
DefaultFieldNamePassword = "password"
DefaultFieldFirstName = "first_name"
DefaultFieldLastName = "last_name"
DefaultFieldEmail = "email"
DefaultFieldPassword = "password"
)

type FormConfig[T any] struct {
type HTTPConfig[T any] struct {
*Config[T]

FirstNameFieldName string // optional (default: DefaultFieldNameFirstName)
LastNameFieldName string // optional (default: DefaultFieldNameLastName)
EmailFieldName string // optional (default: DefaultFieldNameEmail)
PasswordFieldName string // optional (default: DefaultFieldNamePassword)
FieldFirstName string // optional (default: DefaultFieldFirstName)
FieldLastName string // optional (default: DefaultFieldLastName)
FieldEmail string // optional (default: DefaultFieldEmail)
FieldPassword string // optional (default: DefaultFieldPassword)
}

func (c *FormConfig[T]) defaults() {
c.FirstNameFieldName = cmp.Or(c.FirstNameFieldName, DefaultFieldNameEmail)
c.LastNameFieldName = cmp.Or(c.LastNameFieldName, DefaultFieldNameEmail)
c.EmailFieldName = cmp.Or(c.EmailFieldName, DefaultFieldNameEmail)
c.PasswordFieldName = cmp.Or(c.PasswordFieldName, DefaultFieldNameEmail)
func (c *HTTPConfig[T]) defaults() {
c.FieldFirstName = cmp.Or(c.FieldFirstName, DefaultFieldEmail)
c.FieldLastName = cmp.Or(c.FieldLastName, DefaultFieldEmail)
c.FieldEmail = cmp.Or(c.FieldEmail, DefaultFieldEmail)
c.FieldPassword = cmp.Or(c.FieldPassword, DefaultFieldEmail)
if c.Config == nil {
c.Config = NewConfig[T]()
}
}

func (c *FormConfig[T]) assert() {
func (c *HTTPConfig[T]) assert() {
debug.Assert(c.Config != nil, "Config must be set")
}

// NewFormConfig[T] creates a new FormConfig[T] with the given configuration options.
func NewFormConfig[T any](opts ...func(*FormConfig[T])) *FormConfig[T] {
var config FormConfig[T]
// NewHTTPConfig[T] creates a new FormConfig[T] with the given configuration options.
func NewHTTPConfig[T any](opts ...func(*HTTPConfig[T])) *HTTPConfig[T] {
var config HTTPConfig[T]
for _, opt := range opts {
opt(&config)
}
Expand All @@ -63,64 +63,59 @@ func NewFormConfig[T any](opts ...func(*FormConfig[T])) *FormConfig[T] {
return &config
}

// FormHandler[T] is a wrapper around Handler handling HTTP form requests.
type FormHandler[T any] struct {
// HTTPHandler[T] is a wrapper around Handler handling HTTP form requests.
type HTTPHandler[T any] struct {
handler *Handler[T]
config *FormConfig[T]
config *HTTPConfig[T]
parser HTTPRequestParser
}

// NewFormHandler[T] creates a new FormHandler[T] with the given configuration.
// newHTTPHandler[T] creates a new FormHandler[T] with the given configuration.
//
// If config is nil, the default config is used.
func NewFormHandler[T any](pool *pgxpool.Pool, config *FormConfig[T]) *FormHandler[T] {
if config == nil {
config = NewFormConfig[T]()
}
config.assert()

h := FormHandler[T]{
func newHTTPHandler[T any](pool *pgxpool.Pool, config *HTTPConfig[T], parser HTTPRequestParser) *HTTPHandler[T] {
h := HTTPHandler[T]{
NewHandler(pool, config.Config),
config,
parser,
}
h.assert()

debug.Assert(h.handler != nil, "handler must be set")
debug.Assert(h.config != nil, "config must be set")
debug.Assert(h.parser != nil, "parser must be set")

return &h
}

func (h *FormHandler[T]) assert() {
debug.Assert(h.handler != nil, "handler must be set")
}
// NewFormHandler creates a new HTTP handler that handles multipart form requests.
func NewFormHandler[T any](pool *pgxpool.Pool, config *HTTPConfig[T]) *HTTPHandler[T] {
if config == nil {
config = NewHTTPConfig[T]()
}
config.assert()

// userRegistrationForm is the form for user login.
type userRegistrationForm struct {
FirstName string `mod:"trim"`
LastName string `mod:"trim"`
Email string `mod:"trim" validate:"required,email" scrub:"emails"`
Password string `mod:"trim" validate:"required"`
return newHTTPHandler(pool, config, &formParser[T]{config})
}

// userLoginForm is the form for user login.
type userLoginForm struct {
Email string `mod:"trim" validate:"required,email" scrub:"emails"`
Password string `mod:"trim" validate:"required"`
// NewJSONHandler creates a new HTTP handler that handles JSON requests.
func NewJSONHandler[T any](pool *pgxpool.Pool, config *HTTPConfig[T]) *HTTPHandler[T] {
if config == nil {
config = NewHTTPConfig[T]()
}
config.assert()

return newHTTPHandler(pool, config, &jsonParser[T]{config})
}

func (h *FormHandler[T]) parseUserRegistrationForm(
func (h *HTTPHandler[T]) parseUserRegistrationData(
req *http.Request,
) (*userRegistrationForm, error) {
) (*UserRegistrationData, error) {
ctx := req.Context()

if err := req.ParseForm(); err != nil {
form, err := h.parser.ParseUserRegistrationData(req)
if err != nil {
return nil, fmt.Errorf("password: failed to parse request form: %w", err)
}

form := &userRegistrationForm{
FirstName: req.PostFormValue(h.config.FirstNameFieldName),
LastName: req.PostFormValue(h.config.LastNameFieldName),
Email: req.PostFormValue(h.config.EmailFieldName),
Password: req.PostFormValue(h.config.PasswordFieldName),
}

if err := FormModifier.Struct(ctx, form); err != nil {
return nil, fmt.Errorf("password: failed to parse request form: %w", err)
}
Expand All @@ -133,8 +128,8 @@ func (h *FormHandler[T]) parseUserRegistrationForm(
}

// HandleUserRegistration handles a user registration request.
func (h *FormHandler[T]) HandleUserRegistration(r *http.Request) (*shield.User[T], error) {
form, err := h.parseUserRegistrationForm(r)
func (h *HTTPHandler[T]) HandleUserRegistration(r *http.Request) (*shield.User[T], error) {
form, err := h.parseUserRegistrationData(r)
if err != nil {
return nil, httperror.FromError(err, http.StatusBadRequest)
}
Expand All @@ -151,18 +146,14 @@ func (h *FormHandler[T]) HandleUserRegistration(r *http.Request) (*shield.User[T
return result, nil
}

func (h *FormHandler[T]) parseUserLoginForm(req *http.Request) (*userLoginForm, error) {
func (h *HTTPHandler[T]) parseUserLoginData(req *http.Request) (*UserLoginData, error) {
ctx := req.Context()

if err := req.ParseForm(); err != nil {
form, err := h.parser.ParseUserLoginData(req)
if err != nil {
return nil, fmt.Errorf("password: failed to parse request form: %w", err)
}

form := &userLoginForm{
Email: req.PostFormValue(h.config.EmailFieldName),
Password: req.PostFormValue(h.config.PasswordFieldName),
}

if err := FormModifier.Struct(ctx, form); err != nil {
return nil, fmt.Errorf("password: failed to parse request form: %w", err)
}
Expand All @@ -175,8 +166,8 @@ func (h *FormHandler[T]) parseUserLoginForm(req *http.Request) (*userLoginForm,
}

// HandleUserLogin handles a user login request.
func (h *FormHandler[T]) HandleUserLogin(r *http.Request) (*shield.User[T], error) {
form, err := h.parseUserLoginForm(r)
func (h *HTTPHandler[T]) HandleUserLogin(r *http.Request) (*shield.User[T], error) {
form, err := h.parseUserLoginData(r)
if err != nil {
return nil, httperror.FromError(err, http.StatusBadRequest)
}
Expand Down
1 change: 1 addition & 0 deletions shieldpassword/http_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package shieldpassword
89 changes: 89 additions & 0 deletions shieldpassword/request_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package shieldpassword

import (
"encoding/json"
"net/http"
)

var (
_ HTTPRequestParser = (*formParser[any])(nil)
_ HTTPRequestParser = (*jsonParser[any])(nil)
)

// HTTPRequestParser parses HTTP requests to grab user registration and login data.
type HTTPRequestParser interface {
ParseUserRegistrationData(r *http.Request) (*UserRegistrationData, error)
ParseUserLoginData(r *http.Request) (*UserLoginData, error)
}

// UserRegistrationData is the form for user login.
type UserRegistrationData struct {
FirstName string `mod:"trim"`
LastName string `mod:"trim"`
Email string `mod:"trim" validate:"required,email" scrub:"emails"`
Password string `mod:"trim" validate:"required" `
}

// UserLoginData is the form for user login.
type UserLoginData struct {
Email string `mod:"trim" validate:"required,email" scrub:"emails"`
Password string `mod:"trim" validate:"required"`
}

type jsonParser[T any] struct {
config *HTTPConfig[T]
}

func (p *jsonParser[T]) ParseUserRegistrationData(r *http.Request) (*UserRegistrationData, error) {
var m map[string]string
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
return nil, err
}

return &UserRegistrationData{
FirstName: m[p.config.FieldFirstName],
LastName: m[p.config.FieldLastName],
Email: m[p.config.FieldEmail],
Password: m[p.config.FieldPassword],
}, nil
}

func (p *jsonParser[T]) ParseUserLoginData(r *http.Request) (*UserLoginData, error) {
var m map[string]string
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
return nil, err
}

return &UserLoginData{
Email: m[p.config.FieldEmail],
Password: m[p.config.FieldPassword],
}, nil
}

type formParser[T any] struct {
config *HTTPConfig[T]
}

func (p *formParser[T]) ParseUserRegistrationData(r *http.Request) (*UserRegistrationData, error) {
if err := r.ParseForm(); err != nil {
return nil, err
}

return &UserRegistrationData{
FirstName: r.PostFormValue(p.config.FieldFirstName),
LastName: r.PostFormValue(p.config.FieldLastName),
Email: r.PostFormValue(p.config.FieldEmail),
Password: r.PostFormValue(p.config.FieldPassword),
}, nil
}

func (p *formParser[T]) ParseUserLoginData(r *http.Request) (*UserLoginData, error) {
if err := r.ParseForm(); err != nil {
return nil, err
}

return &UserLoginData{
Email: r.PostFormValue(p.config.FieldEmail),
Password: r.PostFormValue(p.config.FieldPassword),
}, nil
}
Loading

0 comments on commit 6a5670c

Please sign in to comment.