Skip to content

Commit

Permalink
feat(cms): fix user registration endpoints & add generateOrmTypes com…
Browse files Browse the repository at this point in the history
…mand to create-tensei-app
  • Loading branch information
bahdcoder committed Nov 13, 2021
1 parent be13a96 commit c35178f
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 29 deletions.
31 changes: 29 additions & 2 deletions packages/cms/main.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React from 'react'
import React, { createContext, useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import './core'
import './load-icons'
import { ThemeProvider as StyledThemeProvider } from 'styled-components'

import { TenseiCtxInterface, CmsRoute } from '@tensei/components'

import '@tensei/eui/dist/eui_theme_tensei_light.css'
import { TenseiCtx } from './pages/components/auth/context'
import { AuthRoutes } from './pages/components/auth/routes'
import { DashboardRoutes } from './pages/components/dashboard/routes'
import { useEuiTheme, EuiThemeProvider } from '@tensei/eui/lib/services/theme'
Expand All @@ -31,9 +34,33 @@ const extensions = {
}

const App: React.FunctionComponent = ({ children }) => {
const [booted, setBooted] = useState(false)
const [routes, setRoutes] = useState<CmsRoute[]>([])
const [user, setUser] = useState<TenseiCtxInterface['user']>(null as any)
const { euiTheme } = useEuiTheme<ThemeExtensions>()

return <StyledThemeProvider theme={euiTheme}>{children}</StyledThemeProvider>
const value = {
user,
setUser,
booted,
setBooted,
routes,
setRoutes
}

window.Tensei.ctx = value

useEffect(() => {
window.Tensei.client.get('csrf')
}, [])

return (
<TenseiCtx.Provider value={value}>
<StyledThemeProvider theme={euiTheme}>
{booted ? children : 'Booting app...'}
</StyledThemeProvider>
</TenseiCtx.Provider>
)
}

ReactDOM.render(
Expand Down
11 changes: 11 additions & 0 deletions packages/cms/pages/components/auth/context/auth-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { TenseiCtxInterface } from '@tensei/components'
import { createContext } from 'react'

export const TenseiCtx = createContext<TenseiCtxInterface>({
user: null as any,
booted: false,
setUser: () => {},
setBooted: () => {},
routes: [],
setRoutes: () => {}
})
1 change: 1 addition & 0 deletions packages/cms/pages/components/auth/context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TenseiCtx } from './auth-context'
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import { Redirect } from 'react-router-dom'

import { TenseiCtx } from '../context'

export const MustBeAuthComponent = (Component: React.FC) => {
const Comp = (props: any) => {
return (
<TenseiCtx.Consumer>
{({ user }) =>
user ? (
<Component {...props} />
) : (
<Redirect
to={
window.Tensei.state.registered
? window.Tensei.getPath('auth/login')
: window.Tensei.getPath('auth/register')
}
/>
)
}
</TenseiCtx.Consumer>
)
}

return Comp
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react'
import { Redirect } from 'react-router-dom'

import { TenseiCtx } from '../context'

export const MustBeNotAuthComponent = (Component: React.FC) => {
const Comp = (props: any) => {
return (
<TenseiCtx.Consumer>
{({ user }) =>
!user ? (
<Component {...props} />
) : (
<Redirect to={window.Tensei.getPath('')} />
)
}
</TenseiCtx.Consumer>
)
}

return Comp
}
53 changes: 28 additions & 25 deletions packages/cms/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,13 @@ class CmsPlugin {

if (adminCount !== 0) {
return done(
{
message: 'Admin user already exists.'
},
[
{
message:
'An administrator already exists. Please join the team by requesting an invitation.',
field: 'email'
}
],
null
)
}
Expand Down Expand Up @@ -456,30 +460,29 @@ class CmsPlugin {
successRedirect: `${this.getApiPath('')}`,
failureRedirect: `${this.getApiPath('auth/register')}`
},
(error, user, info) => {
if (user === false) {
return response.status(422).json([
{
message: 'Please provide your first name.',
field: 'firstName'
},
{
message: 'Please provide your last name.',
field: 'lastName'
},
{
message: 'Please provide your email',
field: 'email'
},
{
message: 'Please provide your password',
field: 'password'
}
])
async (error, user) => {
if (user === false && !error) {
// This is a unique case, where the user did not provide the email or password.
// Passport is weird, so they do not even send the request to the controller when this happens.
// In this scenario, we'll perform validation here on the data, and send the correct validation errors back to the frontend.

const validator = Utils.validator(
self.userResource(),
request.manager,
request.resources
)

const [, payload] = await validator.validate(request.body)

return response.status(422).json({
errors: payload
})
}

if (error || !user) {
return response.status(400).json(error)
if (error) {
return response.status(422).json({
errors: error
})
}

request.logIn(user, error => {
Expand Down
18 changes: 18 additions & 0 deletions packages/create-tensei-app/tasks/generate-orm-types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import execa from 'execa'
import { TaskFn } from '../../contracts'
import { getInstallMessage } from '../../helpers'
import { packages } from '../../schematics/packages'

/**
* Runs the tensei orm:types command after installation and setup.
*/
const task: TaskFn = async (_, logger, { absPath, debug }) => {
try {
await execa('npm', ['run', 'postinstall'], {
cwd: absPath,
...(debug ? { stdio: 'inherit' } : {})
})
} catch {}
}

export default task
5 changes: 5 additions & 0 deletions packages/create-tensei-app/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import copyTemplates from './scaffold/copyTemplates'
import createTsConfig from './scaffold/createTsConfig'
import installDependencies from './install-dependencies'
import createGitIgnore from './scaffold/createGitIgnore'
import generateOrmTypes from './generate-orm-types'

/**
* An array of tasks to be executed in chronological order
Expand All @@ -24,6 +25,10 @@ export const tasks = function () {
{
title: 'Install dependencies',
actions: [installDependencies]
},
{
title: 'Generate types',
actions: [generateOrmTypes]
}
]
}
107 changes: 105 additions & 2 deletions packages/tests/packages/cms/admin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,111 @@ test('cannot register another administrator if a super admin already exists', as
password: user.password + user.password
})

expect(response.status).toBe(400)
expect(response.body.message).toBe('Admin user already exists.')
expect(response.status).toBe(422)
expect(response.body).toEqual({
errors: [
{
message:
'An administrator already exists. Please join the team by requesting an invitation.',
field: 'email'
}
]
})
})

test('cannot register administrator without all valid data', async () => {
const mailerMock = getFakeMailer()

const user = fakeUser()

const {
app,
ctx: {
orm: { em }
}
} = await setup([cms().plugin(), setupFakeMailer(mailerMock)], false)

const client = (SupertestSession(app) as unknown) as SI<any>

const csrf = await getCmsCsrfToken(client)

// Clear all existing administrators and passwordless tokens.
await em.nativeDelete('AdminToken', {})
await em.nativeDelete('AdminUser', {})

const response = await client
.post('/cms/api/auth/register')
.set('X-XSRF-TOKEN', csrf)
.send({})

expect(response.body).toEqual({
errors: [
{
message: 'The firstName is required.',
validation: 'required',
field: 'firstName'
},
{
message: 'The lastName is required.',
validation: 'required',
field: 'lastName'
},
{
message: 'The password is required.',
validation: 'required',
field: 'password'
},
{
message: 'The email is required.',
validation: 'required',
field: 'email'
}
]
})
})

test('cannot register administrator with only some valid data', async () => {
const mailerMock = getFakeMailer()

const user = fakeUser()

const {
app,
ctx: {
orm: { em }
}
} = await setup([cms().plugin(), setupFakeMailer(mailerMock)], false)

const client = (SupertestSession(app) as unknown) as SI<any>

const csrf = await getCmsCsrfToken(client)

// Clear all existing administrators and passwordless tokens.
await em.nativeDelete('AdminToken', {})
await em.nativeDelete('AdminUser', {})

const response = await client
.post('/cms/api/auth/register')
.set('X-XSRF-TOKEN', csrf)
.send({
email: 'x@y.co',
password: 'password-password'
})

expect(response.body).toEqual({
errors: [
{
message: 'The firstName is required.',
validation: 'required',
field: 'firstName'
},
{
message: 'The lastName is required.',
validation: 'required',
field: 'lastName'
}
]
})
})

test('can login an existing administrator user', async () => {
Expand Down

0 comments on commit c35178f

Please sign in to comment.