Skip to content

Commit

Permalink
feat(auth): Added flexible validation checks for User entity email an…
Browse files Browse the repository at this point in the history
…d password

Adds an additional Prisma middleware for validation checks that the user
can disable and/or extend

Closes #88 and fixes #65
  • Loading branch information
shayneczyzewski committed Dec 2, 2021
1 parent bb3964a commit 81000d5
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const LoginForm = () => {
history.push('{= onAuthSucceededRedirectTo =}')
} catch (err) {
console.log(err)
window.alert('Error:' + err.message)
window.alert(`Error: ${err.message} ${(err.data ? '\n' + JSON.stringify(err.data) : '')}`)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const SignupForm = () => {
history.push('{= onAuthSucceededRedirectTo =}')
} catch (err) {
console.log(err)
window.alert('Error:' + err.message)
window.alert(`Error: ${err.message} ${(err.data ? '\n' + JSON.stringify(err.data) : '')}`)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class EntityValidationError extends Error {
constructor (entityName, message, data, ...params) {
super(message, ...params)

if (Error.captureStackTrace) {
Error.captureStackTrace(this, EntityValidationError)
}

this.name = this.constructor.name

if (!entityName) {
throw new Error('entityName has to be a non-empty string.')
}
this.entityName = entityName

if (data) {
this.data = data
}
}
}

export default EntityValidationError
36 changes: 36 additions & 0 deletions waspc/data/Generator/templates/server/src/dbClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Prisma from '@prisma/client'
{=# isAuthEnabled =}
import { hashPassword } from './core/auth.js'
import EntityValidationError from './core/EntityValidationError.js'
{=/ isAuthEnabled =}

{=# isAuthEnabled =}
Expand All @@ -12,6 +13,41 @@ const createDbClient = () => {
const prismaClient = new Prisma.PrismaClient()

{=# isAuthEnabled =}

prismaClient.$use(async (params, next) => {
// Ensure strong plaintext password. Must come before hashing middleware.
// Throws an EntityValidationError on the first validation that fails.
if (params.model === '{= userEntityUpper =}' && params.action === 'create') {
const data = params.args.data || {}

const defaultValidations = [
{ name: 'email must be present', fn: data => !!data.email },
{ name: 'password must be present', fn: data => !!data.password },
{ name: 'password must be at least 8 characters', fn: data => data.password.length >= 8 },
{ name: 'password must contain a number', fn: data => /\d/.test(data.password) },
]

let validations = params.args._waspSkipDefaultValidations ? [] : defaultValidations
if (Array.isArray(params.args._waspCustomValidations)) {
validations = validations.concat(params.args._waspCustomValidations)
}

for (const validation of validations) {
if (!validation.fn(data)) {
throw new EntityValidationError('{= userEntityUpper =}', validation.name)
}
}

// Remove from downstream Prisma processing to avoid "Unknown arg" error
delete params.args._waspSkipDefaultValidations
delete params.args._waspCustomValidations
}

const result = next(params)

return result
})

prismaClient.$use(async (params, next) => {
// Make sure password is always hashed before storing to the database.
if (params.model === '{= userEntityUpper =}') {
Expand Down
16 changes: 14 additions & 2 deletions waspc/data/Generator/templates/server/src/routes/auth/signup.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
{{={= =}=}}
import Prisma from '@prisma/client'
import prisma from '../../dbClient.js'
import { handleRejection } from '../../utils.js'
import EntityValidationError from '../../core/EntityValidationError.js'
import HttpError from '../../core/HttpError.js'

export default handleRejection(async (req, res) => {
const userFields = req.body || {}

await prisma.{= userEntityLower =}.create({ data: userFields })
try {
await prisma.{= userEntityLower =}.create({ data: userFields })
res.send()
} catch(e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new HttpError(422, 'Save failed', { prisma_error_code: e.code })
} else if (e instanceof EntityValidationError) {
throw new HttpError(422, 'Validation failed', { entity: e.entityName, message: e.message })
}
throw new HttpError(500)
}

res.send()
})
1 change: 1 addition & 0 deletions waspc/src/Wasp/Generator/ServerGenerator.hs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ genSrcDir wasp =
[ [C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|app.js|]],
[C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|server.js|]],
[C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|utils.js|]],
[C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|core/EntityValidationError.js|]],
[C.copySrcTmplAsIs $ C.asTmplSrcFile [relfile|core/HttpError.js|]],
[genDbClient wasp],
[genConfigFile wasp],
Expand Down
18 changes: 17 additions & 1 deletion web/docs/language/basic-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ Check out this [section of our Todo app tutorial](/docs/tutorials/todo-app/auth#
`EmailAndPassword` authentication method makes it possible to signup/login into the app by using email address and a password.
This method requires that `userEntity` specified in `auth` element contains `email: string` and `password: string` fields.

We provide basic validations out of the box, which you can customize as shown below. Default validations are:
- `email`: non-empty
- `password`: non-empty, at least 8 characters, and contains a number

#### High-level API

The quickest way to get started is by using the following API generated by Wasp:
Expand Down Expand Up @@ -365,9 +369,21 @@ export const signUp = async (args, context) => {
```

:::info
You don't need to worry about hashing the password yourself! Even when you are using Prisma's client directly and calling `create()` with a plain-text password, Wasp put middleware in place that takes care of hashing it before storing it to the database.
You don't need to worry about hashing the password yourself! Even when you are using Prisma's client directly and calling `create()` with a plain-text password, Wasp put middleware in place that takes care of hashing it before storing it to the database. An additional middleware also performs field validation.
:::

To disable default validations, or add your own, you can do:
```js
const newUser = context.entities.User.create({
data: { email: 'some@email.com', password: 'this will be hashed!' },
_waspSkipDefaultValidations: true, // defaults to false
_waspCustomValidations: [
{ name: 'password should not be password', fn: data => data.password !== 'password' },
// More can be added below (note: it stops on first to return false)
]
})
```

#### Specification

### `login()`
Expand Down

0 comments on commit 81000d5

Please sign in to comment.