Skip to content

Commit bd37fe2

Browse files
author
Chris Stockton
committed
feat: add Before & After user created hooks
Add Before & After user created to pkg internal/hooks/v0hooks.
1 parent d5f5436 commit bd37fe2

File tree

3 files changed

+227
-7
lines changed

3 files changed

+227
-7
lines changed

internal/hooks/v0hooks/manager.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,33 @@ func configByName(
5858
return &cfg.MFAVerificationAttempt, true
5959
case PasswordVerification:
6060
return &cfg.PasswordVerificationAttempt, true
61+
case BeforeUserCreated:
62+
return &cfg.BeforeUserCreated, true
63+
case AfterUserCreated:
64+
return &cfg.AfterUserCreated, true
6165
default:
6266
return nil, false
6367
}
6468
}
6569

70+
func (o *Manager) BeforeUserCreated(
71+
ctx context.Context,
72+
tx *storage.Connection,
73+
req *BeforeUserCreatedInput,
74+
res *BeforeUserCreatedOutput,
75+
) error {
76+
return o.dispatch(ctx, &o.config.Hook.BeforeUserCreated, tx, req, res)
77+
}
78+
79+
func (o *Manager) AfterUserCreated(
80+
ctx context.Context,
81+
tx *storage.Connection,
82+
req *AfterUserCreatedInput,
83+
res *AfterUserCreatedOutput,
84+
) error {
85+
return o.dispatch(ctx, &o.config.Hook.AfterUserCreated, tx, req, res)
86+
}
87+
6688
func (o *Manager) InvokeHook(
6789
conn *storage.Connection,
6890
r *http.Request,

internal/hooks/v0hooks/manager_test.go

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/supabase/auth/internal/e2e"
1414
"github.com/supabase/auth/internal/hooks/hookshttp"
1515
"github.com/supabase/auth/internal/hooks/hookspgfunc"
16+
"github.com/supabase/auth/internal/models"
1617
)
1718

1819
type M = map[string]any
@@ -31,9 +32,13 @@ func TestHooks(t *testing.T) {
3132
mr := NewManager(globalCfg, httpDr, pgfuncDr)
3233
now := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
3334

35+
httpReq := httptest.NewRequestWithContext(
36+
ctx, "GET", "http://localhost/test", nil)
37+
3438
type testCase struct {
3539
desc string
3640
setup func()
41+
run func(*testCase, *Manager) error
3742
sql string
3843
req any
3944
res any
@@ -218,6 +223,122 @@ func TestHooks(t *testing.T) {
218223
$$ language plpgsql;`,
219224
},
220225

226+
{
227+
desc: "pass - before_user_created",
228+
setup: func() {
229+
globalCfg.Hook.BeforeUserCreated =
230+
conf.ExtensibilityPointConfiguration{
231+
URI: `pg-functions://postgres/auth/` +
232+
`v0hooks_test_before_user_created`,
233+
HookName: `"auth"."v0hooks_test_before_user_created"`,
234+
}
235+
},
236+
run: func(tc *testCase, mr *Manager) error {
237+
return mr.BeforeUserCreated(
238+
ctx, db,
239+
tc.req.(*BeforeUserCreatedInput),
240+
tc.res.(*BeforeUserCreatedOutput),
241+
)
242+
},
243+
req: NewBeforeUserCreatedInput(httpReq, &models.User{}),
244+
res: &BeforeUserCreatedOutput{},
245+
exp: &BeforeUserCreatedOutput{},
246+
sql: `
247+
create or replace function
248+
v0hooks_test_before_user_created(input jsonb)
249+
returns json as $$
250+
begin
251+
return '{}'::jsonb;
252+
end; $$ language plpgsql;`,
253+
},
254+
255+
{
256+
desc: "pass - before_user_created reject",
257+
setup: func() {
258+
globalCfg.Hook.BeforeUserCreated =
259+
conf.ExtensibilityPointConfiguration{
260+
URI: `pg-functions://postgres/auth/` +
261+
`v0hooks_test_before_user_created_reject`,
262+
HookName: `"auth"."v0hooks_test_before_user_created_reject"`,
263+
}
264+
},
265+
run: func(tc *testCase, mr *Manager) error {
266+
return mr.BeforeUserCreated(
267+
ctx, db,
268+
tc.req.(*BeforeUserCreatedInput),
269+
tc.res.(*BeforeUserCreatedOutput),
270+
)
271+
},
272+
req: NewBeforeUserCreatedInput(httpReq, &models.User{}),
273+
res: &BeforeUserCreatedOutput{},
274+
exp: &BeforeUserCreatedOutput{Decision: "reject"},
275+
sql: `
276+
create or replace function
277+
v0hooks_test_before_user_created_reject(input jsonb)
278+
returns json as $$
279+
begin
280+
return '{"decision": "reject"}'::jsonb;
281+
end; $$ language plpgsql;`,
282+
},
283+
284+
{
285+
desc: "pass - before_user_created reject with message",
286+
setup: func() {
287+
globalCfg.Hook.BeforeUserCreated =
288+
conf.ExtensibilityPointConfiguration{
289+
URI: `pg-functions://postgres/auth/` +
290+
`v0hooks_test_before_user_created_reject_msg`,
291+
HookName: `"auth"."v0hooks_test_before_user_created_reject_msg"`,
292+
}
293+
},
294+
run: func(tc *testCase, mr *Manager) error {
295+
return mr.BeforeUserCreated(
296+
ctx, db,
297+
tc.req.(*BeforeUserCreatedInput),
298+
tc.res.(*BeforeUserCreatedOutput),
299+
)
300+
},
301+
req: NewBeforeUserCreatedInput(httpReq, &models.User{}),
302+
res: &BeforeUserCreatedOutput{},
303+
exp: &BeforeUserCreatedOutput{Decision: "reject", Message: "test case"},
304+
sql: `
305+
create or replace function
306+
v0hooks_test_before_user_created_reject_msg(input jsonb)
307+
returns json as $$
308+
begin
309+
return '{"decision": "reject", "message": "test case"}'::jsonb;
310+
end; $$ language plpgsql;`,
311+
},
312+
313+
{
314+
desc: "pass - after_user_created",
315+
setup: func() {
316+
globalCfg.Hook.AfterUserCreated =
317+
conf.ExtensibilityPointConfiguration{
318+
URI: `pg-functions://postgres/auth/` +
319+
`v0hooks_test_after_user_created`,
320+
HookName: `"auth"."v0hooks_test_after_user_created"`,
321+
}
322+
},
323+
run: func(tc *testCase, mr *Manager) error {
324+
return mr.AfterUserCreated(
325+
ctx, db,
326+
tc.req.(*AfterUserCreatedInput),
327+
tc.res.(*AfterUserCreatedOutput),
328+
)
329+
},
330+
req: NewAfterUserCreatedInput(httpReq, &models.User{}),
331+
res: &AfterUserCreatedOutput{},
332+
exp: &AfterUserCreatedOutput{},
333+
sql: `
334+
create or replace function
335+
v0hooks_test_after_user_created(input jsonb)
336+
returns json as $$
337+
begin
338+
return '{}'::jsonb;
339+
end; $$ language plpgsql;`,
340+
},
341+
221342
// fail
222343
{
223344
desc: "fail - customize_access_token - error propagation",
@@ -404,7 +525,12 @@ func TestHooks(t *testing.T) {
404525
}
405526

406527
htr := httptest.NewRequestWithContext(ctx, "POST", "/api", nil)
407-
err := mr.InvokeHook(db, htr, tc.req, tc.res)
528+
var err error
529+
if tc.run == nil {
530+
err = mr.InvokeHook(db, htr, tc.req, tc.res)
531+
} else {
532+
err = tc.run(&tc, mr)
533+
}
408534
if tc.errStr != "" {
409535
require.Error(t, err)
410536
require.Contains(t, err.Error(), tc.errStr)
@@ -433,6 +559,12 @@ func TestConfig(t *testing.T) {
433559
PasswordVerificationAttempt: conf.ExtensibilityPointConfiguration{
434560
URI: "http:localhost/" + string(PasswordVerification),
435561
},
562+
BeforeUserCreated: conf.ExtensibilityPointConfiguration{
563+
URI: "http:localhost/" + string(BeforeUserCreated),
564+
},
565+
AfterUserCreated: conf.ExtensibilityPointConfiguration{
566+
URI: "http:localhost/" + string(AfterUserCreated),
567+
},
436568
},
437569
}
438570
cfg := &globalCfg.Hook
@@ -457,6 +589,10 @@ func TestConfig(t *testing.T) {
457589
name: MFAVerification, exp: &cfg.MFAVerificationAttempt},
458590
{cfg: cfg, ok: true,
459591
name: PasswordVerification, exp: &cfg.PasswordVerificationAttempt},
592+
{cfg: cfg, ok: true,
593+
name: BeforeUserCreated, exp: &cfg.BeforeUserCreated},
594+
{cfg: cfg, ok: true,
595+
name: AfterUserCreated, exp: &cfg.AfterUserCreated},
460596
}
461597
for idx, test := range tests {
462598
t.Logf("test #%v - exp ok %v with cfg %v from name %v",

internal/hooks/v0hooks/v0hooks.go

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package v0hooks
22

33
import (
4+
"net/http"
5+
"time"
6+
47
"github.com/gofrs/uuid"
58
"github.com/golang-jwt/jwt/v5"
69
"github.com/supabase/auth/internal/mailer"
710
"github.com/supabase/auth/internal/models"
11+
"github.com/supabase/auth/internal/utilities"
812
)
913

1014
type Name string
@@ -15,13 +19,76 @@ const (
1519
CustomizeAccessToken Name = "customize-access-token"
1620
MFAVerification Name = "mfa-verification"
1721
PasswordVerification Name = "password-verification"
22+
BeforeUserCreated Name = "before-user-created"
23+
AfterUserCreated Name = "after-user-created"
1824
)
1925

20-
// Hook Names
2126
const (
2227
HookRejection = "reject"
2328
)
2429

30+
const (
31+
DefaultMFAHookRejectionMessage = "Further MFA verification attempts will be rejected."
32+
DefaultPasswordHookRejectionMessage = "Further password verification attempts will be rejected."
33+
)
34+
35+
type Metadata struct {
36+
UUID uuid.UUID `json:"uuid"`
37+
Time time.Time `json:"time"`
38+
39+
// Hook name
40+
Name Name `json:"name,omitempty"`
41+
42+
// IP Address of the request, if present
43+
IPAddress string `json:"ip_address,omitempty"`
44+
}
45+
46+
func NewMetadata(r *http.Request, name Name) *Metadata {
47+
return &Metadata{
48+
UUID: uuid.Must(uuid.NewV4()),
49+
Time: time.Now(),
50+
IPAddress: utilities.GetIPAddress(r),
51+
Name: name,
52+
}
53+
}
54+
55+
type BeforeUserCreatedInput struct {
56+
Header *Metadata `json:"header"`
57+
User *models.User `json:"user"`
58+
}
59+
60+
func NewBeforeUserCreatedInput(
61+
r *http.Request,
62+
user *models.User,
63+
) *BeforeUserCreatedInput {
64+
return &BeforeUserCreatedInput{
65+
Header: NewMetadata(r, BeforeUserCreated),
66+
User: user,
67+
}
68+
}
69+
70+
type BeforeUserCreatedOutput struct {
71+
Decision string `json:"decision"`
72+
Message string `json:"message"`
73+
}
74+
75+
type AfterUserCreatedInput struct {
76+
Header *Metadata `json:"header"`
77+
User *models.User `json:"user"`
78+
}
79+
80+
func NewAfterUserCreatedInput(
81+
r *http.Request,
82+
user *models.User,
83+
) *AfterUserCreatedInput {
84+
return &AfterUserCreatedInput{
85+
Header: NewMetadata(r, AfterUserCreated),
86+
User: user,
87+
}
88+
}
89+
90+
type AfterUserCreatedOutput struct{}
91+
2592
// TODO(joel): Move this to phone package
2693
type SMS struct {
2794
OTP string `json:"otp,omitempty"`
@@ -90,8 +157,3 @@ type SendEmailInput struct {
90157

91158
type SendEmailOutput struct {
92159
}
93-
94-
const (
95-
DefaultMFAHookRejectionMessage = "Further MFA verification attempts will be rejected."
96-
DefaultPasswordHookRejectionMessage = "Further password verification attempts will be rejected."
97-
)

0 commit comments

Comments
 (0)