Skip to content

Commit

Permalink
RSC: dbAuth in kitchen-sink (#10907)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe authored Jul 3, 2024
1 parent 4bdc2b3 commit 614d36a
Show file tree
Hide file tree
Showing 32 changed files with 1,216 additions and 40 deletions.
3 changes: 3 additions & 0 deletions __fixtures__/test-project-rsc-kitchen-sink/.env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ PRISMA_HIDE_UPDATE_MESSAGE=true
# Most applications want "debug" or "info" during dev, "trace" when you have issues and "warn" in production.
# Ordered by how verbose they are: trace | debug | info | warn | error | silent
# LOG_LEVEL=debug

# This is obviously *not* where this would go in a production app
SESSION_SECRET=YKhZE3KzbLqWPp6UKNuoZKctObqxK05fmG74S9dmK5s=
Binary file modified __fixtures__/test-project-rsc-kitchen-sink/api/db/dev.db
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"hashedPassword" TEXT NOT NULL,
"salt" TEXT NOT NULL,
"resetToken" TEXT,
"resetTokenExpiresAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
11 changes: 11 additions & 0 deletions __fixtures__/test-project-rsc-kitchen-sink/api/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,14 @@ model EmptyUser {
email String @unique
name String?
}

model User {
id Int @id @default(autoincrement())
email String @unique
hashedPassword String
salt String
resetToken String?
resetTokenExpiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
4 changes: 2 additions & 2 deletions __fixtures__/test-project-rsc-kitchen-sink/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@redwoodjs/api": "7.0.0-canary.1011",
"@redwoodjs/graphql-server": "7.0.0-canary.1011"
"@redwoodjs/api": "8.0.0-canary.861",
"@redwoodjs/graphql-server": "8.0.0-canary.861"
}
}
207 changes: 207 additions & 0 deletions __fixtures__/test-project-rsc-kitchen-sink/api/src/functions/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import type { APIGatewayProxyEvent, Context } from 'aws-lambda'

import { DbAuthHandler } 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'

export const handler = async (
event: APIGatewayProxyEvent,
context: Context
) => {
const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
// 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.
//
// Note that this return value is sent to the client in *plain text*
// so don't include anything you wouldn't want prying eyes to see. The
// `user` here has been sanitized to only include the fields listed in
// `allowedUserFields` so it should be safe to return as-is.
handler: (user, _resetToken) => {
// TODO: Send user an email/message with a link to reset their password,
// including the `resetToken`. The URL should look something like:
// `http://localhost:8910/reset-password?resetToken=${resetToken}`

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: 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) => {
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: 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) => {
return true
},

// 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 {
name: 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: ({
username,
hashedPassword,
salt,
userAttributes: _userAttributes,
}) => {
return db.user.create({
data: {
email: username,
hashedPassword: hashedPassword,
salt: salt,
// name: userAttributes.name
},
})
},

// 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) => {
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: 'email',
hashedPassword: 'hashedPassword',
salt: 'salt',
resetToken: 'resetToken',
resetTokenExpiresAt: 'resetTokenExpiresAt',
},

// 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: 'Strict',
Secure: process.env.NODE_ENV !== 'development',

// If you need to allow other domains (besides the api side) access to
// the dbAuth session cookie:
// Domain: 'example.com',
},
name: cookieName,
},

forgotPassword: forgotPasswordOptions,
login: loginOptions,
resetPassword: resetPasswordOptions,
signup: signupOptions,
})

return await authHandler.invoke()
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'

import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'

import { cookieName, getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

const authDecoder = createAuthDecoder(cookieName)

export const handler = createGraphQLHandler({
authDecoder,
getCurrentUser,
loggerConfig: { logger, options: {} },
directives,
sdls,
Expand Down
Loading

0 comments on commit 614d36a

Please sign in to comment.