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) }) })