diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md
index 38c832352a..2f16412ceb 100644
--- a/waspc/ChangeLog.md
+++ b/waspc/ChangeLog.md
@@ -1,18 +1,11 @@
# Changelog
-<<<<<<< HEAD
-=======
-
->>>>>>> f5d285d61 (Updates changelog)
## 0.14.0 (TBD)
### 🎉 New Features
- Simplified Auth User API: Introduced a simpler API for accessing user auth fields (for example `username`, `email`, `isEmailVerified`) directly on the `user` object, eliminating the need for helper functions.
-<<<<<<< HEAD
- Improved API for calling Operations (Queries and Actions) directly.
-=======
->>>>>>> f5d285d61 (Updates changelog)
- Auth Hooks: you can now hook into the auth process with `onBeforeSignup`, `onAfterSignup` hooks. You can also modify the OAuth redirect URL with `onBeforeOAuthRedirect` hook and get the provider token with `onAfterOAuthTokenReceived` hook.
```wasp
diff --git a/waspc/headless-test/examples/todoApp/migrations/20240508125445_add_headless_test_property/migration.sql b/waspc/headless-test/examples/todoApp/migrations/20240508125445_add_headless_test_property/migration.sql
new file mode 100644
index 0000000000..5c83e174db
--- /dev/null
+++ b/waspc/headless-test/examples/todoApp/migrations/20240508125445_add_headless_test_property/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "isOnAfterSignupHookCalled" BOOLEAN NOT NULL DEFAULT false;
diff --git a/waspc/headless-test/examples/todoApp/src/auth/hooks.ts b/waspc/headless-test/examples/todoApp/src/auth/hooks.ts
new file mode 100644
index 0000000000..1daf36bf0b
--- /dev/null
+++ b/waspc/headless-test/examples/todoApp/src/auth/hooks.ts
@@ -0,0 +1,20 @@
+import { HttpError } from 'wasp/server'
+import type {
+ OnAfterSignupHookFn,
+ OnBeforeSignupHookFn,
+} from 'wasp/server/auth'
+
+export const onBeforeSignup: OnBeforeSignupHookFn = async (args) => {
+ if (args.providerId.providerUserId === 'notallowed@email.com') {
+ throw new HttpError(403, 'On Before Signup Hook disallows this email.')
+ }
+}
+
+export const onAfterSignup: OnAfterSignupHookFn = async (args) => {
+ await args.prisma.user.update({
+ where: { id: args.user.id },
+ data: {
+ isOnAfterSignupHookCalled: true,
+ },
+ })
+}
diff --git a/waspc/headless-test/examples/todoApp/src/client/pages/ProfilePage.tsx b/waspc/headless-test/examples/todoApp/src/client/pages/ProfilePage.tsx
index d2c486d99d..d0e06f0c9a 100644
--- a/waspc/headless-test/examples/todoApp/src/client/pages/ProfilePage.tsx
+++ b/waspc/headless-test/examples/todoApp/src/client/pages/ProfilePage.tsx
@@ -19,12 +19,14 @@ export const ProfilePage = ({ user }: { user: User }) => {
Hello {user.getFirstProviderUserId()}! Your status is{' '}
- {user.identities.email && user.identities.email.isEmailVerified
- ? 'verfied'
- : 'unverified'}
+ {user.identities.email?.isEmailVerified ? 'verfied' : 'unverified'}
.
+
+ Value of user.isOnAfterSignupHookCalled
is{' '}
+ {user.isOnAfterSignupHookCalled ? 'true' : 'false'}.
+
Go to dashboard
>
diff --git a/waspc/headless-test/examples/todoApp/todoApp.wasp b/waspc/headless-test/examples/todoApp/todoApp.wasp
index 518fad8bf0..d172ef4f14 100644
--- a/waspc/headless-test/examples/todoApp/todoApp.wasp
+++ b/waspc/headless-test/examples/todoApp/todoApp.wasp
@@ -23,7 +23,9 @@ app todoApp {
google: {}
},
onAuthFailedRedirectTo: "/login",
- onAuthSucceededRedirectTo: "/profile"
+ onAuthSucceededRedirectTo: "/profile",
+ onBeforeSignup: import { onBeforeSignup } from "@src/auth/hooks.js",
+ onAfterSignup: import { onAfterSignup } from "@src/auth/hooks.js",
},
server: {
setupFn: import setup from "@src/server/serverSetup.js",
@@ -50,6 +52,7 @@ app todoApp {
entity User {=psl
id Int @id @default(autoincrement())
+ isOnAfterSignupHookCalled Boolean @default(false)
// Business logic
tasks Task[]
psl=}
diff --git a/waspc/headless-test/tests/auth-hooks.spec.ts b/waspc/headless-test/tests/auth-hooks.spec.ts
new file mode 100644
index 0000000000..d8a8d00f45
--- /dev/null
+++ b/waspc/headless-test/tests/auth-hooks.spec.ts
@@ -0,0 +1,50 @@
+import { test, expect } from '@playwright/test'
+import {
+ generateRandomCredentials,
+ performLogin,
+ performSignup,
+} from './helpers'
+
+test.describe('auth hooks', () => {
+ test.describe.configure({ mode: 'serial' })
+
+ /*
+ We set up the "before signup hook" to throw an error for a specific email address.
+ */
+ test('before signup hook works', async ({ page }) => {
+ const emailThatThrowsError = 'notallowed@email.com'
+ const password = '12345678'
+
+ await performSignup(page, {
+ email: emailThatThrowsError,
+ password,
+ })
+
+ await expect(page.locator('body')).toContainText(
+ 'On Before Signup Hook disallows this email.',
+ )
+ })
+
+ /*
+ We set up the "after signup hook" to set a value in the user object.
+ */
+ test('after signup hook works', async ({ page }) => {
+ const { email, password } = generateRandomCredentials()
+
+ await performSignup(page, {
+ email,
+ password,
+ })
+
+ await performLogin(page, {
+ email,
+ password,
+ })
+
+ await expect(page).toHaveURL('/profile')
+
+ await expect(page.locator('body')).toContainText(
+ 'Value of user.isOnAfterSignupHookCalled is true.',
+ )
+ })
+})
diff --git a/waspc/headless-test/tests/helpers.ts b/waspc/headless-test/tests/helpers.ts
new file mode 100644
index 0000000000..e16cba34ca
--- /dev/null
+++ b/waspc/headless-test/tests/helpers.ts
@@ -0,0 +1,43 @@
+import type { Page } from '@playwright/test'
+
+export async function performSignup(
+ page: Page,
+ { email, password }: { email: string; password: string },
+) {
+ await page.goto('/signup')
+
+ await page.waitForSelector('text=Create a new account')
+
+ await page.locator("input[type='email']").fill(email)
+ await page.locator("input[type='password']").fill(password)
+ await page.locator('button').click()
+}
+
+export async function performLogin(
+ page: Page,
+ {
+ email,
+ password,
+ }: {
+ email: string
+ password: string
+ },
+) {
+ await page.goto('/login')
+
+ await page.waitForSelector('text=Log in to your account')
+
+ await page.locator("input[type='email']").fill(email)
+ await page.locator("input[type='password']").fill(password)
+ await page.getByRole('button', { name: 'Log in' }).click()
+}
+
+export function generateRandomCredentials(): {
+ email: string
+ password: string
+} {
+ return {
+ email: `test${Math.random().toString(36).substring(7)}@test.com`,
+ password: '12345678',
+ }
+}
diff --git a/waspc/headless-test/tests/simple.spec.ts b/waspc/headless-test/tests/simple.spec.ts
index 3109bf9efb..d84560e366 100644
--- a/waspc/headless-test/tests/simple.spec.ts
+++ b/waspc/headless-test/tests/simple.spec.ts
@@ -1,4 +1,9 @@
import { test, expect } from '@playwright/test'
+import {
+ generateRandomCredentials,
+ performLogin,
+ performSignup,
+} from './helpers'
test('has title', async ({ page }) => {
await page.goto('/')
@@ -6,8 +11,7 @@ test('has title', async ({ page }) => {
await expect(page).toHaveTitle(/ToDo App/)
})
test.describe('signup and login', () => {
- const randomEmail = `test${Math.random().toString(36).substring(7)}@test.com`
- const password = '12345678'
+ const { email, password } = generateRandomCredentials()
test.describe.configure({ mode: 'serial' })
@@ -22,13 +26,10 @@ test.describe('signup and login', () => {
})
test('can sign up', async ({ page }) => {
- await page.goto('/signup')
-
- await page.waitForSelector('text=Create a new account')
-
- await page.locator("input[type='email']").fill(randomEmail)
- await page.locator("input[type='password']").fill(password)
- await page.locator('button').click()
+ await performSignup(page, {
+ email,
+ password,
+ })
await expect(page.locator('body')).toContainText(
`You've signed up successfully! Check your email for the confirmation link.`,
@@ -36,24 +37,23 @@ test.describe('signup and login', () => {
})
test('can log in and create a task', async ({ page }) => {
- await page.goto('/login')
-
- await page.waitForSelector('text=Log in to your account')
-
- await page.locator("input[type='email']").fill(randomEmail)
- await page.locator("input[type='password']").fill('12345678xxx')
- await page.getByRole('button', { name: 'Log in' }).click()
+ await performLogin(page, {
+ email,
+ password: '12345678xxx',
+ })
- await expect(page.locator('body')).toContainText(`Invalid credentials`)
+ await expect(page.locator('body')).toContainText('Invalid credentials')
- await page.locator("input[type='password']").fill(password)
- await page.getByRole('button', { name: 'Log in' }).click()
+ await performLogin(page, {
+ email,
+ password,
+ })
await expect(page).toHaveURL('/profile')
await page.goto('/')
- const randomTask = 'New Task ' + Math.random().toString(36).substring(7)
+ const randomTask = `New Task ${Math.random().toString(36).substring(7)}`
await page.locator("input[type='text']").fill(randomTask)
await page.getByText('Create new task').click()
diff --git a/waspc/headless-test/tests/user-api.spec.ts b/waspc/headless-test/tests/user-api.spec.ts
index 6fd61a969f..f64e474575 100644
--- a/waspc/headless-test/tests/user-api.spec.ts
+++ b/waspc/headless-test/tests/user-api.spec.ts
@@ -1,22 +1,18 @@
import { test, expect } from '@playwright/test'
+import { generateRandomCredentials, performSignup } from './helpers'
test.describe('user API', () => {
- const randomEmail = `test${Math.random().toString(36).substring(7)}@test.com`
- const password = '12345678'
+ const { email, password } = generateRandomCredentials()
test.describe.configure({ mode: 'serial' })
test.beforeAll(async ({ browser }) => {
const page = await browser.newPage()
- // Sign up
- await page.goto('/signup')
-
- await page.waitForSelector('text=Create a new account')
-
- await page.locator("input[type='email']").fill(randomEmail)
- await page.locator("input[type='password']").fill(password)
- await page.locator('button').click()
+ await performSignup(page, {
+ email,
+ password,
+ })
})
test('user API works on the client', async ({ page }) => {
@@ -24,16 +20,16 @@ test.describe('user API', () => {
await page.waitForSelector('text=Log in to your account')
- await page.locator("input[type='email']").fill(randomEmail)
+ await page.locator("input[type='email']").fill(email)
await page.locator("input[type='password']").fill(password)
await page.getByRole('button', { name: 'Log in' }).click()
await page.waitForSelector('text=Profile page')
await expect(page.locator('body')).toContainText(
- `Hello ${randomEmail}! Your status is verfied`,
+ `Hello ${email}! Your status is verfied`,
)
- await expect(page.locator('a[href="/profile"]')).toContainText(randomEmail)
+ await expect(page.locator('a[href="/profile"]')).toContainText(email)
})
})