All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 39s
248 lines
8.6 KiB
TypeScript
Executable File
248 lines
8.6 KiB
TypeScript
Executable File
import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
|
|
|
|
import { isProduction } from '@redwoodjs/api/logger'
|
|
import {
|
|
DbAuthHandler,
|
|
PasswordValidationError,
|
|
} from '@redwoodjs/auth-dbauth-api'
|
|
import type { DbAuthHandlerOptions, UserType } from '@redwoodjs/auth-dbauth-api'
|
|
|
|
import { cookieName } from 'src/lib/auth'
|
|
import { db } from 'src/lib/db'
|
|
import { censorEmail, sendEmail } from 'src/lib/email'
|
|
|
|
function getCommonCookieDomain(domain: string, apiDomain: string): string {
|
|
const splitDomain1 = domain.split('.').reverse()
|
|
const splitDomain2 = apiDomain.split('.').reverse()
|
|
const commonParts: string[] = []
|
|
|
|
for (let i = 0; i < Math.min(splitDomain1.length, splitDomain2.length); i++) {
|
|
if (splitDomain1[i] === splitDomain2[i]) commonParts.push(splitDomain1[i])
|
|
else break
|
|
}
|
|
|
|
if (commonParts.length < 2)
|
|
throw new Error('Domains do not share the same TLD')
|
|
|
|
return commonParts.reverse().join('.')
|
|
}
|
|
|
|
export const handler = async (
|
|
event: APIGatewayProxyEvent,
|
|
context: Context
|
|
) => {
|
|
const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
|
|
handler: async (user, resetToken) => {
|
|
const domain = isProduction
|
|
? process.env.ADDRESS_PROD
|
|
: process.env.ADDRESS_DEV
|
|
const text = `If this wasn't you, please disregard this email.
|
|
|
|
Hello,
|
|
|
|
You are receiving this email because a password reset was requested.
|
|
Enter the following URL to begin resetting your password:
|
|
|
|
${domain}/reset-password?resetToken=${resetToken}
|
|
`
|
|
|
|
const html = text.replaceAll('\n', '<br />')
|
|
|
|
try {
|
|
await sendEmail({
|
|
to: user.email,
|
|
subject: 'Password Reset Request',
|
|
text,
|
|
html,
|
|
})
|
|
|
|
return {
|
|
email: censorEmail(user.email),
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
error: `Error: ${error.code} ${error.responseCode}`,
|
|
}
|
|
}
|
|
},
|
|
|
|
// 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: 'Account not found',
|
|
// if the user somehow gets around client validation
|
|
usernameRequired: 'Email is required',
|
|
},
|
|
}
|
|
|
|
const loginOptions: DbAuthHandlerOptions['login'] = {
|
|
// 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) => 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',
|
|
},
|
|
|
|
// How long a user will remain logged in, in seconds
|
|
expires: 60 * 60 * 24 * 365 * 10,
|
|
}
|
|
|
|
const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
|
|
// handler() is invoked after the password has been successfully updated in
|
|
// the database. Returning anything truthy will automatically log the user
|
|
// in. Return `false` otherwise, and in the Reset Password page redirect the
|
|
// user to the login page.
|
|
handler: (_user) => false,
|
|
|
|
// If `false` then the new password MUST be different from 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',
|
|
},
|
|
}
|
|
|
|
interface UserAttributes {
|
|
email: string
|
|
}
|
|
|
|
const signupOptions: DbAuthHandlerOptions<
|
|
UserType,
|
|
UserAttributes
|
|
>['signup'] = {
|
|
// 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: async ({ username, hashedPassword, salt, userAttributes }) => {
|
|
// Only one admin account should exist
|
|
if ((await db.user.count()) >= 1)
|
|
return { error: 'Admin account already exists' }
|
|
|
|
if (
|
|
new RegExp(
|
|
/^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
|
|
).test(userAttributes.email)
|
|
)
|
|
return db.user.create({
|
|
data: {
|
|
username: username,
|
|
email: userAttributes.email,
|
|
hashedPassword: hashedPassword,
|
|
salt: salt,
|
|
},
|
|
})
|
|
else return { error: 'Invalid email' }
|
|
},
|
|
|
|
// Include any format checks for password here. Return `true` if the
|
|
// password is valid, otherwise throw a `PasswordValidationError`.
|
|
// Import the error along with `DbAuthHandler` from `@redwoodjs/api` above.
|
|
passwordValidation: (password) => {
|
|
if (password.length < 8)
|
|
throw new PasswordValidationError(
|
|
'Password must be at least 8 characters'
|
|
)
|
|
else return true
|
|
},
|
|
|
|
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.
|
|
// i.e. 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: 'username',
|
|
hashedPassword: 'hashedPassword',
|
|
salt: 'salt',
|
|
resetToken: 'resetToken',
|
|
resetTokenExpiresAt: 'resetTokenExpiresAt',
|
|
},
|
|
|
|
cors: {
|
|
origin: isProduction ? process.env.ADDRESS_PROD : process.env.ADDRESS_DEV,
|
|
credentials: isProduction,
|
|
methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'],
|
|
},
|
|
|
|
// A list of fields on your user object that are safe to return to the
|
|
// client when invoking a handler that returns a user (like forgotPassword
|
|
// and signup). This list should be as small as possible to be sure not to
|
|
// leak any sensitive information to the client.
|
|
allowedUserFields: ['id', 'email'],
|
|
|
|
// Specifies attributes on the cookie that dbAuth sets in order to remember
|
|
// who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
|
|
cookie: {
|
|
attributes: {
|
|
HttpOnly: true,
|
|
Path: '/',
|
|
SameSite: isProduction ? 'None' : 'Strict',
|
|
Secure: isProduction,
|
|
Domain: isProduction
|
|
? getCommonCookieDomain(process.env.DOMAIN, process.env.API_DOMAIN)
|
|
: 'localhost',
|
|
},
|
|
name: cookieName,
|
|
},
|
|
|
|
forgotPassword: forgotPasswordOptions,
|
|
login: loginOptions,
|
|
resetPassword: resetPasswordOptions,
|
|
signup: signupOptions,
|
|
})
|
|
|
|
return await authHandler.invoke()
|
|
}
|