Adds dbAuth

This commit is contained in:
Rob Cameron
2022-02-08 14:48:37 -08:00
parent 5dc0ccea4d
commit 9ca3046cc4
8 changed files with 639 additions and 40 deletions

149
api/src/functions/auth.js Normal file
View File

@@ -0,0 +1,149 @@
import { db } from 'src/lib/db'
import { DbAuthHandler } from '@redwoodjs/api'
export const handler = async (event, context) => {
const forgotPasswordOptions = {
// handler() is invoked after verifying that a user was found with the given
// username. This is where you can send the user an email with a link to
// reset their password. With the default dbAuth routes and field names, the
// URL to reset the password will be:
//
// https://example.com/reset-password?resetToken=${user.resetToken}
//
// Whatever is returned from this function will be returned from
// the `forgotPassword()` function that is destructured from `useAuth()`
// You could use this return value to, for example, show the email
// address in a toast message so the user will know it worked and where
// to look for the email.
handler: (user) => {
return user
},
// How long the resetToken is valid for, in seconds (default is 24 hours)
expires: 60 * 60 * 24,
errors: {
// for security reasons you may want to be vague here rather than expose
// the fact that the email address wasn't found (prevents fishing for
// valid email addresses)
usernameNotFound: 'Username not found',
// if the user somehow gets around client validation
usernameRequired: 'Username is required',
},
}
const loginOptions = {
// handler() is called after finding the user that matches the
// username/password provided at login, but before actually considering them
// logged in. The `user` argument will be the user in the database that
// matched the username/password.
//
// If you want to allow this user to log in simply return the user.
//
// If you want to prevent someone logging in for another reason (maybe they
// didn't validate their email yet), throw an error and it will be returned
// by the `logIn()` function from `useAuth()` in the form of:
// `{ message: 'Error message' }`
handler: (user) => {
return user
},
errors: {
usernameOrPasswordMissing: 'Both username and password are required',
usernameNotFound: 'Username ${username} not found',
// For security reasons you may want to make this the same as the
// usernameNotFound error so that a malicious user can't use the error
// to narrow down if it's the username or password that's incorrect
incorrectPassword: 'Incorrect password for ${username}',
},
// How long a user will remain logged in, in seconds
expires: 60 * 60 * 24 * 365 * 10,
}
const resetPasswordOptions = {
// handler() is invoked after the password has been successfully updated in
// the database. Returning anything truthy will automatically logs the user
// in. Return `false` otherwise, and in the Reset Password page redirect the
// user to the login page.
handler: (user) => {
return user
},
// If `false` then the new password MUST be different than the current one
allowReusedPassword: true,
errors: {
// the resetToken is valid, but expired
resetTokenExpired: 'resetToken is expired',
// no user was found with the given resetToken
resetTokenInvalid: 'resetToken is invalid',
// the resetToken was not present in the URL
resetTokenRequired: 'resetToken is required',
// new password is the same as the old password (apparently they did not forget it)
reusedPassword: 'Must choose a new password',
},
}
const signupOptions = {
// Whatever you want to happen to your data on new user signup. Redwood will
// check for duplicate usernames before calling this handler. At a minimum
// you need to save the `username`, `hashedPassword` and `salt` to your
// user table. `userAttributes` contains any additional object members that
// were included in the object given to the `signUp()` function you got
// from `useAuth()`.
//
// If you want the user to be immediately logged in, return the user that
// was created.
//
// If this handler throws an error, it will be returned by the `signUp()`
// function in the form of: `{ error: 'Error message' }`.
//
// If this returns anything else, it will be returned by the
// `signUp()` function in the form of: `{ message: 'String here' }`.
handler: ({ username, hashedPassword, salt, userAttributes }) => {
return db.user.create({
data: {
email: username,
hashedPassword: hashedPassword,
salt: salt,
// name: userAttributes.name
},
})
},
errors: {
// `field` will be either "username" or "password"
fieldMissing: '${field} is required',
usernameTaken: 'Username `${username}` already in use',
},
}
const authHandler = new DbAuthHandler(event, context, {
// Provide prisma db client
db: db,
// The name of the property you'd call on `db` to access your user table.
// ie. if your Prisma model is named `User` this value would be `user`, as in `db.user`
authModelAccessor: 'user',
// A map of what dbAuth calls a field to what your database calls it.
// `id` is whatever column you use to uniquely identify a user (probably
// something like `id` or `userId` or even `email`)
authFields: {
id: 'id',
username: 'email',
hashedPassword: 'hashedPassword',
salt: 'salt',
resetToken: 'resetToken',
resetTokenExpiresAt: 'resetTokenExpiresAt',
},
forgotPassword: forgotPasswordOptions,
login: loginOptions,
resetPassword: resetPasswordOptions,
signup: signupOptions,
})
return await authHandler.invoke()
}

View File

@@ -1,36 +1,28 @@
import { parseJWT } from '@redwoodjs/api'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'
import { logger } from 'src/lib/logger'
import { db } from './db'
/**
* getCurrentUser returns the user information together with
* an optional collection of roles used by requireAuth() to check
* if the user is authenticated or has role-based access
* The session object sent in as the first argument to getCurrentUser() will
* have a single key `id` containing the unique ID of the logged in user
* (whatever field you set as `authFields.id` in your auth function config).
* You'll need to update the call to `db` below if you use a different model
* name or unique field name, for example:
*
* @param decoded - The decoded access token containing user info and JWT claims like `sub`. Note could be null.
* @param { token, SupportedAuthTypes type } - The access token itself as well as the auth provider type
* @param { APIGatewayEvent event, Context context } - An object which contains information from the invoker
* such as headers and cookies, and the context information about the invocation such as IP Address
* return await db.profile.findUnique({ where: { email: session.id } })
* ───┬─── ──┬──
* model accessor ─┘ unique id field name ─┘
*
* @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
* !! BEWARE !! Anything returned from this function will be available to the
* client--it becomes the content of `currentUser` on the web side (as well as
* `context.currentUser` on the api side). You should carefully add additional
* fields to the `select` object below once you've decided they are safe to be
* seen if someone were to open the Web Inspector in their browser.
*/
export const getCurrentUser = async (
decoded,
{ _token, _type },
{ _event, _context }
) => {
if (!decoded) {
logger.warn('Missing decoded user')
return null
}
const { roles } = parseJWT({ decoded })
if (roles) {
return { ...decoded, roles }
}
return { ...decoded }
export const getCurrentUser = async (session) => {
return await db.user.findUnique({
where: { id: session.id },
select: { id: true },
})
}
/**
@@ -60,13 +52,14 @@ export const hasRole = ({ roles }) => {
return false
}
// If your User model includes roles, uncomment the role checks on currentUser
if (roles) {
if (Array.isArray(roles)) {
return context.currentUser.roles?.some((r) => roles.includes(r))
// return context.currentUser.roles?.some((r) => roles.includes(r))
}
if (typeof roles === 'string') {
return context.currentUser.roles?.includes(roles)
// return context.currentUser.roles?.includes(roles)
}
// roles not found