Skip to content

Commit 6a5670c

Browse files
feat: add json & form handlers for password and passwordreset modules; drop shieldtempl; use nix for CI
1 parent f9b482e commit 6a5670c

File tree

12 files changed

+301
-191
lines changed

12 files changed

+301
-191
lines changed

.github/workflows/test.yaml

+8-20
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ on:
88
jobs:
99
test:
1010
runs-on: ubuntu-latest
11-
strategy:
12-
matrix:
13-
go-version: ["1.22.x"]
11+
permissions:
12+
id-token: "write"
13+
contents: "read"
1414

1515
services:
1616
postgres:
@@ -29,20 +29,8 @@ jobs:
2929

3030
steps:
3131
- uses: actions/checkout@v4
32-
- name: Setup Go ${{ matrix.go-version }}
33-
uses: actions/setup-go@v5
34-
with:
35-
go-version: ${{ matrix.go-version }}
36-
37-
- name: Restore Cache
38-
uses: actions/cache@v4
39-
with:
40-
path: |
41-
~/.cache/go-build
42-
~/go/pkg/mod
43-
key: ${{ runner.os }}-v1-go-${{ hashFiles('**/go.sum') }}
44-
restore-keys: |
45-
${{ runner.os }}-go-
46-
47-
- name: Run Tests
48-
run: go test -v ./...
32+
- uses: DeterminateSystems/nix-installer-action@main
33+
- uses: DeterminateSystems/magic-nix-cache-action@main
34+
- uses: DeterminateSystems/flake-checker-action@main
35+
- name: Test
36+
run: nix develop --command go test -count=1 -race -v ./...

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ module go.inout.gg/shield
33
go 1.23.1
44

55
require (
6-
github.com/a-h/templ v0.2.778
76
github.com/coreos/go-oidc/v3 v3.11.0
87
github.com/go-playground/mold/v4 v4.5.0
98
github.com/go-playground/validator/v10 v10.22.1
@@ -30,6 +29,7 @@ require (
3029
github.com/go-playground/universal-translator v0.18.1 // indirect
3130
github.com/go-webauthn/x v0.1.14 // indirect
3231
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
32+
github.com/google/go-cmp v0.6.0 // indirect
3333
github.com/google/go-tpm v0.9.1 // indirect
3434
github.com/gosimple/slug v1.13.1 // indirect
3535
github.com/gosimple/unidecode v1.0.1 // indirect

go.sum

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
2-
github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
31
github.com/caarlos0/env/v11 v11.1.0 h1:a5qZqieE9ZfzdvbbdhTalRrHT5vu/4V1/ad1Ka6frhI=
42
github.com/caarlos0/env/v11 v11.1.0/go.mod h1:LwgkYk1kDvfGpHthrWWLof3Ny7PezzFwS4QrsJdHTMo=
53
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=

shieldpassword/.test.env

-1
This file was deleted.

shieldpassword/form_handler.go shieldpassword/http_handler.go

+56-65
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,38 @@ var (
2222
)
2323

2424
const (
25-
DefaultFieldNameFirstName = "first_name"
26-
DefaultFieldNameLastName = "last_name"
27-
DefaultFieldNameEmail = "email"
28-
DefaultFieldNamePassword = "password"
25+
DefaultFieldFirstName = "first_name"
26+
DefaultFieldLastName = "last_name"
27+
DefaultFieldEmail = "email"
28+
DefaultFieldPassword = "password"
2929
)
3030

31-
type FormConfig[T any] struct {
31+
type HTTPConfig[T any] struct {
3232
*Config[T]
3333

34-
FirstNameFieldName string // optional (default: DefaultFieldNameFirstName)
35-
LastNameFieldName string // optional (default: DefaultFieldNameLastName)
36-
EmailFieldName string // optional (default: DefaultFieldNameEmail)
37-
PasswordFieldName string // optional (default: DefaultFieldNamePassword)
34+
FieldFirstName string // optional (default: DefaultFieldFirstName)
35+
FieldLastName string // optional (default: DefaultFieldLastName)
36+
FieldEmail string // optional (default: DefaultFieldEmail)
37+
FieldPassword string // optional (default: DefaultFieldPassword)
3838
}
3939

40-
func (c *FormConfig[T]) defaults() {
41-
c.FirstNameFieldName = cmp.Or(c.FirstNameFieldName, DefaultFieldNameEmail)
42-
c.LastNameFieldName = cmp.Or(c.LastNameFieldName, DefaultFieldNameEmail)
43-
c.EmailFieldName = cmp.Or(c.EmailFieldName, DefaultFieldNameEmail)
44-
c.PasswordFieldName = cmp.Or(c.PasswordFieldName, DefaultFieldNameEmail)
40+
func (c *HTTPConfig[T]) defaults() {
41+
c.FieldFirstName = cmp.Or(c.FieldFirstName, DefaultFieldEmail)
42+
c.FieldLastName = cmp.Or(c.FieldLastName, DefaultFieldEmail)
43+
c.FieldEmail = cmp.Or(c.FieldEmail, DefaultFieldEmail)
44+
c.FieldPassword = cmp.Or(c.FieldPassword, DefaultFieldEmail)
4545
if c.Config == nil {
4646
c.Config = NewConfig[T]()
4747
}
4848
}
4949

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

54-
// NewFormConfig[T] creates a new FormConfig[T] with the given configuration options.
55-
func NewFormConfig[T any](opts ...func(*FormConfig[T])) *FormConfig[T] {
56-
var config FormConfig[T]
54+
// NewHTTPConfig[T] creates a new FormConfig[T] with the given configuration options.
55+
func NewHTTPConfig[T any](opts ...func(*HTTPConfig[T])) *HTTPConfig[T] {
56+
var config HTTPConfig[T]
5757
for _, opt := range opts {
5858
opt(&config)
5959
}
@@ -63,64 +63,59 @@ func NewFormConfig[T any](opts ...func(*FormConfig[T])) *FormConfig[T] {
6363
return &config
6464
}
6565

66-
// FormHandler[T] is a wrapper around Handler handling HTTP form requests.
67-
type FormHandler[T any] struct {
66+
// HTTPHandler[T] is a wrapper around Handler handling HTTP form requests.
67+
type HTTPHandler[T any] struct {
6868
handler *Handler[T]
69-
config *FormConfig[T]
69+
config *HTTPConfig[T]
70+
parser HTTPRequestParser
7071
}
7172

72-
// NewFormHandler[T] creates a new FormHandler[T] with the given configuration.
73+
// newHTTPHandler[T] creates a new FormHandler[T] with the given configuration.
7374
//
7475
// If config is nil, the default config is used.
75-
func NewFormHandler[T any](pool *pgxpool.Pool, config *FormConfig[T]) *FormHandler[T] {
76-
if config == nil {
77-
config = NewFormConfig[T]()
78-
}
79-
config.assert()
80-
81-
h := FormHandler[T]{
76+
func newHTTPHandler[T any](pool *pgxpool.Pool, config *HTTPConfig[T], parser HTTPRequestParser) *HTTPHandler[T] {
77+
h := HTTPHandler[T]{
8278
NewHandler(pool, config.Config),
8379
config,
80+
parser,
8481
}
85-
h.assert()
82+
83+
debug.Assert(h.handler != nil, "handler must be set")
84+
debug.Assert(h.config != nil, "config must be set")
85+
debug.Assert(h.parser != nil, "parser must be set")
8686

8787
return &h
8888
}
8989

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

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

102-
// userLoginForm is the form for user login.
103-
type userLoginForm struct {
104-
Email string `mod:"trim" validate:"required,email" scrub:"emails"`
105-
Password string `mod:"trim" validate:"required"`
100+
// NewJSONHandler creates a new HTTP handler that handles JSON requests.
101+
func NewJSONHandler[T any](pool *pgxpool.Pool, config *HTTPConfig[T]) *HTTPHandler[T] {
102+
if config == nil {
103+
config = NewHTTPConfig[T]()
104+
}
105+
config.assert()
106+
107+
return newHTTPHandler(pool, config, &jsonParser[T]{config})
106108
}
107109

108-
func (h *FormHandler[T]) parseUserRegistrationForm(
110+
func (h *HTTPHandler[T]) parseUserRegistrationData(
109111
req *http.Request,
110-
) (*userRegistrationForm, error) {
112+
) (*UserRegistrationData, error) {
111113
ctx := req.Context()
112-
113-
if err := req.ParseForm(); err != nil {
114+
form, err := h.parser.ParseUserRegistrationData(req)
115+
if err != nil {
114116
return nil, fmt.Errorf("password: failed to parse request form: %w", err)
115117
}
116118

117-
form := &userRegistrationForm{
118-
FirstName: req.PostFormValue(h.config.FirstNameFieldName),
119-
LastName: req.PostFormValue(h.config.LastNameFieldName),
120-
Email: req.PostFormValue(h.config.EmailFieldName),
121-
Password: req.PostFormValue(h.config.PasswordFieldName),
122-
}
123-
124119
if err := FormModifier.Struct(ctx, form); err != nil {
125120
return nil, fmt.Errorf("password: failed to parse request form: %w", err)
126121
}
@@ -133,8 +128,8 @@ func (h *FormHandler[T]) parseUserRegistrationForm(
133128
}
134129

135130
// HandleUserRegistration handles a user registration request.
136-
func (h *FormHandler[T]) HandleUserRegistration(r *http.Request) (*shield.User[T], error) {
137-
form, err := h.parseUserRegistrationForm(r)
131+
func (h *HTTPHandler[T]) HandleUserRegistration(r *http.Request) (*shield.User[T], error) {
132+
form, err := h.parseUserRegistrationData(r)
138133
if err != nil {
139134
return nil, httperror.FromError(err, http.StatusBadRequest)
140135
}
@@ -151,18 +146,14 @@ func (h *FormHandler[T]) HandleUserRegistration(r *http.Request) (*shield.User[T
151146
return result, nil
152147
}
153148

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

157-
if err := req.ParseForm(); err != nil {
152+
form, err := h.parser.ParseUserLoginData(req)
153+
if err != nil {
158154
return nil, fmt.Errorf("password: failed to parse request form: %w", err)
159155
}
160156

161-
form := &userLoginForm{
162-
Email: req.PostFormValue(h.config.EmailFieldName),
163-
Password: req.PostFormValue(h.config.PasswordFieldName),
164-
}
165-
166157
if err := FormModifier.Struct(ctx, form); err != nil {
167158
return nil, fmt.Errorf("password: failed to parse request form: %w", err)
168159
}
@@ -175,8 +166,8 @@ func (h *FormHandler[T]) parseUserLoginForm(req *http.Request) (*userLoginForm,
175166
}
176167

177168
// HandleUserLogin handles a user login request.
178-
func (h *FormHandler[T]) HandleUserLogin(r *http.Request) (*shield.User[T], error) {
179-
form, err := h.parseUserLoginForm(r)
169+
func (h *HTTPHandler[T]) HandleUserLogin(r *http.Request) (*shield.User[T], error) {
170+
form, err := h.parseUserLoginData(r)
180171
if err != nil {
181172
return nil, httperror.FromError(err, http.StatusBadRequest)
182173
}

shieldpassword/http_handler_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package shieldpassword

shieldpassword/request_parser.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package shieldpassword
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
)
7+
8+
var (
9+
_ HTTPRequestParser = (*formParser[any])(nil)
10+
_ HTTPRequestParser = (*jsonParser[any])(nil)
11+
)
12+
13+
// HTTPRequestParser parses HTTP requests to grab user registration and login data.
14+
type HTTPRequestParser interface {
15+
ParseUserRegistrationData(r *http.Request) (*UserRegistrationData, error)
16+
ParseUserLoginData(r *http.Request) (*UserLoginData, error)
17+
}
18+
19+
// UserRegistrationData is the form for user login.
20+
type UserRegistrationData struct {
21+
FirstName string `mod:"trim"`
22+
LastName string `mod:"trim"`
23+
Email string `mod:"trim" validate:"required,email" scrub:"emails"`
24+
Password string `mod:"trim" validate:"required" `
25+
}
26+
27+
// UserLoginData is the form for user login.
28+
type UserLoginData struct {
29+
Email string `mod:"trim" validate:"required,email" scrub:"emails"`
30+
Password string `mod:"trim" validate:"required"`
31+
}
32+
33+
type jsonParser[T any] struct {
34+
config *HTTPConfig[T]
35+
}
36+
37+
func (p *jsonParser[T]) ParseUserRegistrationData(r *http.Request) (*UserRegistrationData, error) {
38+
var m map[string]string
39+
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
40+
return nil, err
41+
}
42+
43+
return &UserRegistrationData{
44+
FirstName: m[p.config.FieldFirstName],
45+
LastName: m[p.config.FieldLastName],
46+
Email: m[p.config.FieldEmail],
47+
Password: m[p.config.FieldPassword],
48+
}, nil
49+
}
50+
51+
func (p *jsonParser[T]) ParseUserLoginData(r *http.Request) (*UserLoginData, error) {
52+
var m map[string]string
53+
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
54+
return nil, err
55+
}
56+
57+
return &UserLoginData{
58+
Email: m[p.config.FieldEmail],
59+
Password: m[p.config.FieldPassword],
60+
}, nil
61+
}
62+
63+
type formParser[T any] struct {
64+
config *HTTPConfig[T]
65+
}
66+
67+
func (p *formParser[T]) ParseUserRegistrationData(r *http.Request) (*UserRegistrationData, error) {
68+
if err := r.ParseForm(); err != nil {
69+
return nil, err
70+
}
71+
72+
return &UserRegistrationData{
73+
FirstName: r.PostFormValue(p.config.FieldFirstName),
74+
LastName: r.PostFormValue(p.config.FieldLastName),
75+
Email: r.PostFormValue(p.config.FieldEmail),
76+
Password: r.PostFormValue(p.config.FieldPassword),
77+
}, nil
78+
}
79+
80+
func (p *formParser[T]) ParseUserLoginData(r *http.Request) (*UserLoginData, error) {
81+
if err := r.ParseForm(); err != nil {
82+
return nil, err
83+
}
84+
85+
return &UserLoginData{
86+
Email: r.PostFormValue(p.config.FieldEmail),
87+
Password: r.PostFormValue(p.config.FieldPassword),
88+
}, nil
89+
}

0 commit comments

Comments
 (0)