Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dbAuth): Prompt to generate dbAuth pages #10865

Merged
merged 5 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changesets/10865.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- feat(dbAuth): Prompt to generate dbAuth pages (#10865) by @Tobbe

When setting up dbAuth we'll now prompt if the user also wants to generate pages for login, signup, password reset etc. We only prompt if no existing pages exist.
240 changes: 232 additions & 8 deletions packages/auth-providers/dbAuth/setup/src/__tests__/setup.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
import path from 'node:path'

import { vol } from 'memfs'
import prompts from 'prompts'

import { type AuthHandlerArgs } from '@redwoodjs/cli-helpers'

jest.mock('fs', () => require('memfs').fs)

import { createAuthDecoderFunction } from '../setupHandler'
import { createAuthDecoderFunction, handler } from '../setupHandler'

const RWJS_CWD = process.env.RWJS_CWD
const redwoodProjectPath = '/redwood-app'
const mockLoginPagePath = path.join(
redwoodProjectPath,
'web/src/pages/LoginPage/LoginPage.tsx',
)

jest.mock('../setupData', () => ({
notes: '',
extraTask: undefined,
}))
jest.mock('prompts', () => {
return {
__esModule: true,
default: jest.fn(async (args: any) => {
return {
[args.name]: false,
}
}),
}
})

jest.mock('../shared', () => ({
hasModel: () => false,
hasAuthPages: () => {
return require('fs').existsSync(mockLoginPagePath)
},
generateAuthPagesTask: () => undefined,
}))

jest.mock('@redwoodjs/cli-helpers', () => {
return {
getGraphqlPath: () => {
return redwoodProjectPath + '/api/src/functions/graphql.ts'
},
addEnvVarTask: () => undefined,
getPaths: () => ({
base: redwoodProjectPath,
}),
Expand All @@ -34,6 +52,12 @@ jest.mock('@redwoodjs/cli-helpers', () => {
bold: (str: string) => str,
underline: (str: string) => str,
},
// I wish I could have used something like
// jest.requireActual(@redwoodjs/cli-helpers) here, but I couldn't because
// jest doesn't support ESM
standardAuthHandler: async (args: AuthHandlerArgs) => {
args.notes && console.log(`\n ${args.notes.join('\n ')}\n`)
},
}
})

Expand All @@ -45,13 +69,24 @@ afterAll(() => {
process.env.RWJS_CWD = RWJS_CWD
})

beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {})
})

afterEach(() => {
jest.mocked(console).log.mockRestore?.()
jest.mocked(prompts).mockClear?.()
})

describe('dbAuth setup command', () => {
it('does not duplicate authDecoder creation', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')
const graphqlTsPath = 'api/src/functions/graphql.ts'

vol.fromJSON(
{
[path.resolve(__dirname, '../../package.json')]:
'{ "version": "0.0.0" }',
'api/src/functions/graphql.ts': `
[packageJsonPath]: '{ "version": "0.0.0" }',
[graphqlTsPath]: `
import { createGraphQLHandler } from '@redwoodjs/graphql-server'

import directives from 'src/directives/**/*.{js,ts}'
Expand Down Expand Up @@ -91,4 +126,193 @@ export const handler = createGraphQLHandler({
vol.toJSON()[redwoodProjectPath + '/api/src/functions/graphql.ts']
expect(updatedGraphqlTs).toEqual(updatedGraphqlTs2)
})

it('prompts to generate pages', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
},
redwoodProjectPath,
)

await handler({
webauthn: false,
createUserModel: false,
generateAuthPages: null,
force: false,
})

expect(jest.mocked(prompts).mock.calls[0][0].message).toMatch(
/Generate auth pages/,
)
})

it('does not prompt to generate pages when pages already exist', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
[mockLoginPagePath]: 'export default () => <div>Login</div>',
},
redwoodProjectPath,
)

await handler({
webauthn: false,
createUserModel: false,
generateAuthPages: null,
force: false,
})

expect(jest.mocked(prompts)).not.toHaveBeenCalled()
})

describe('One More Thing... message', () => {
describe('page generation hint', () => {
it('is not included if page generation was already done as part of the setup process', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
},
redwoodProjectPath,
)

await handler({
webauthn: false,
createUserModel: false,
generateAuthPages: true,
force: false,
})

expect(jest.mocked(console).log.mock.calls[0][0]).not.toContain(
'yarn rw generate dbAuth',
)
})

it('is not included for WebAuthn if page generation was already done as part of the setup process', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
},
redwoodProjectPath,
)

await handler({
webauthn: true,
createUserModel: false,
generateAuthPages: true,
force: false,
})

expect(jest.mocked(console).log.mock.calls[0][0]).not.toContain(
'yarn rw generate dbAuth',
)
})

it('is not included if page generation and model generation was already done as part of the setup process', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
},
redwoodProjectPath,
)

await handler({
webauthn: false,
createUserModel: true,
generateAuthPages: true,
force: false,
})

expect(jest.mocked(console).log.mock.calls[0][0]).not.toContain(
'yarn rw generate dbAuth',
)
})

it('is not included for WebAuthn if page generation and model generation was already done as part of the setup process', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
},
redwoodProjectPath,
)

await handler({
webauthn: true,
createUserModel: true,
generateAuthPages: true,
force: false,
})

expect(jest.mocked(console).log.mock.calls[0][0]).not.toContain(
'yarn rw generate dbAuth',
)
})

it('is included if page generation was not done as part of the setup process', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
},
redwoodProjectPath,
)

await handler({
webauthn: false,
createUserModel: false,
generateAuthPages: false,
force: false,
})

const logs: string[][] = [...jest.mocked(console).log.mock.calls]
const oneMoreThingMessage = logs.find((log) => {
return log[0].includes('Done! But you have a little more work to do')
})?.[0]

// Included exactly once
expect(
oneMoreThingMessage?.match(/yarn rw generate dbAuth/g),
).toHaveLength(1)
})

it('is included for WebAuthn if page generation was not done as part of the setup process', async () => {
const packageJsonPath = path.resolve(__dirname, '../../package.json')

vol.fromJSON(
{
[packageJsonPath]: '{ "version": "0.0.0" }',
},
redwoodProjectPath,
)

await handler({
webauthn: true,
createUserModel: false,
generateAuthPages: false,
force: false,
})

const firstLogMessage = jest.mocked(console).log.mock.calls[0][0]

// Included exactly once
expect(firstLogMessage.match(/and WebAuthn prompts/g)).toHaveLength(1)
expect(firstLogMessage.match(/yarn rw generate dbAuth/g)).toHaveLength(
1,
)
})
})
})
})
7 changes: 7 additions & 0 deletions packages/auth-providers/dbAuth/setup/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export function builder(yargs: yargs.Argv) {
description: 'Create a User database model',
type: 'boolean',
})
.option('generateAuthPages', {
alias: 'g',
default: null,
description: 'Generate auth pages (login, signup, etc.)',
type: 'boolean',
})
.epilogue(
`Also see the ${terminalLink(
'Redwood CLI Reference',
Expand All @@ -35,6 +41,7 @@ export function builder(yargs: yargs.Argv) {
export interface Args {
webauthn: boolean | null
createUserModel: boolean | null
generateAuthPages: boolean | null
force: boolean
}

Expand Down
8 changes: 3 additions & 5 deletions packages/auth-providers/dbAuth/setup/src/setupData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,6 @@ export const notes = [
'change this secret to a new value and deploy. To create a new secret, run:',
'',
' yarn rw generate secret',
'',
"Need simple Login, Signup and Forgot Password pages? We've got a generator",
'for those as well:',
'',
' yarn rw generate dbAuth',
]

export const notesCreatedUserModel = [
Expand All @@ -117,6 +112,9 @@ export const notesCreatedUserModel = [
'change this secret to a new value and deploy. To create a new secret, run:',
'',
' yarn rw generate secret',
]

export const noteGenerate = [
'',
"Need simple Login, Signup and Forgot Password pages? We've got a generator",
'for those as well:',
Expand Down
Loading