Skip to content

Commit 1dbf539

Browse files
Josh-Walker-GMTobbe
andcommitted
fix(context): Refactor context (#9371)
Co-authored-by: Tobbe Lundberg <[email protected]>
1 parent 33e21ed commit 1dbf539

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1001
-171
lines changed

__fixtures__/test-project/api/src/__tests__/context.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ test('Set a mock user on the context', async () => {
1010
})
1111

1212
test('Context is isolated between tests', () => {
13-
expect(context).toStrictEqual({ currentUser: undefined })
13+
expect(context).toStrictEqual({})
1414
})

packages/api-server/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@fastify/http-proxy": "9.3.0",
3333
"@fastify/static": "6.12.0",
3434
"@fastify/url-data": "5.4.0",
35+
"@redwoodjs/context": "6.6.0",
3536
"@redwoodjs/project-config": "6.6.0",
3637
"ansi-colors": "4.1.3",
3738
"chalk": "4.1.2",

packages/api-server/src/__tests__/fastify.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ jest.mock('fastify', () => {
88
return jest.fn(() => {
99
return {
1010
register: () => {},
11+
addHook: () => {},
1112
}
1213
})
1314
})

packages/api-server/src/fastify.ts

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import path from 'path'
44
import type { FastifyInstance, FastifyServerOptions } from 'fastify'
55
import Fastify from 'fastify'
66

7+
import type { GlobalContext } from '@redwoodjs/context'
8+
import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store'
79
import { getPaths, getConfig } from '@redwoodjs/project-config'
810

911
import type { FastifySideConfigFn } from './types'
@@ -60,6 +62,11 @@ export const createFastifyInstance = (
6062

6163
const fastify = Fastify(options || config || DEFAULT_OPTIONS)
6264

65+
// Ensure that each request has a unique global context
66+
fastify.addHook('onRequest', (_req, _reply, done) => {
67+
getAsyncStoreInstance().run(new Map<string, GlobalContext>(), done)
68+
})
69+
6370
return fastify
6471
}
6572

packages/babel-config/src/__tests__/api.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ describe('api', () => {
212212
},
213213
{
214214
members: ['context'],
215-
path: '@redwoodjs/graphql-server',
215+
path: '@redwoodjs/context',
216216
},
217217
],
218218
},

packages/babel-config/src/__tests__/prebuildApiFile.test.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -443,9 +443,7 @@ describe('api prebuild ', () => {
443443
})
444444

445445
it('auto imports', () => {
446-
expect(code).toContain(
447-
'import { context } from "@redwoodjs/graphql-server"'
448-
)
446+
expect(code).toContain('import { context } from "@redwoodjs/context"')
449447
expect(code).toContain('import gql from "graphql-tag"')
450448
})
451449
})

packages/babel-config/src/api.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ export const getApiSideBabelPlugins = (
110110
path: 'graphql-tag',
111111
},
112112
{
113-
// import { context } from '@redwoodjs/graphql-server'
113+
// import { context } from '@redwoodjs/context'
114114
members: ['context'],
115-
path: '@redwoodjs/graphql-server',
115+
path: '@redwoodjs/context',
116116
},
117117
],
118118
},
@@ -144,10 +144,25 @@ export const getApiSideBabelConfigPath = () => {
144144
}
145145
}
146146

147+
export const getApiSideBabelOverrides = () => {
148+
const overrides = [
149+
// Apply context wrapping to all functions
150+
{
151+
// match */api/src/functions/*.js|ts
152+
test: /.+api(?:[\\|/])src(?:[\\|/])functions(?:[\\|/]).+.(?:js|ts)$/,
153+
plugins: [
154+
require('./plugins/babel-plugin-redwood-context-wrapping').default,
155+
],
156+
},
157+
].filter(Boolean)
158+
return overrides as TransformOptions[]
159+
}
160+
147161
export const getApiSideDefaultBabelConfig = () => {
148162
return {
149163
presets: getApiSideBabelPresets(),
150164
plugins: getApiSideBabelPlugins(),
165+
overrides: getApiSideBabelOverrides(),
151166
extends: getApiSideBabelConfigPath(),
152167
babelrc: false,
153168
ignore: ['node_modules'],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { DbAuthHandler, DbAuthHandlerOptions } from '@redwoodjs/auth-dbauth-api'
2+
3+
import { db } from 'src/lib/db'
4+
5+
export const handler = async (
6+
event,
7+
context
8+
) => {
9+
const forgotPasswordOptions = {
10+
// handler() is invoked after verifying that a user was found with the given
11+
// username. This is where you can send the user an email with a link to
12+
// reset their password. With the default dbAuth routes and field names, the
13+
// URL to reset the password will be:
14+
//
15+
// https://example.com/reset-password?resetToken=${user.resetToken}
16+
//
17+
// Whatever is returned from this function will be returned from
18+
// the `forgotPassword()` function that is destructured from `useAuth()`
19+
// You could use this return value to, for example, show the email
20+
// address in a toast message so the user will know it worked and where
21+
// to look for the email.
22+
handler: (user) => {
23+
return user
24+
},
25+
26+
// How long the resetToken is valid for, in seconds (default is 24 hours)
27+
expires: 60 * 60 * 24,
28+
29+
errors: {
30+
// for security reasons you may want to be vague here rather than expose
31+
// the fact that the email address wasn't found (prevents fishing for
32+
// valid email addresses)
33+
usernameNotFound: 'Username not found',
34+
// if the user somehow gets around client validation
35+
usernameRequired: 'Username is required',
36+
},
37+
}
38+
39+
const loginOptions = {
40+
// handler() is called after finding the user that matches the
41+
// username/password provided at login, but before actually considering them
42+
// logged in. The `user` argument will be the user in the database that
43+
// matched the username/password.
44+
//
45+
// If you want to allow this user to log in simply return the user.
46+
//
47+
// If you want to prevent someone logging in for another reason (maybe they
48+
// didn't validate their email yet), throw an error and it will be returned
49+
// by the `logIn()` function from `useAuth()` in the form of:
50+
// `{ message: 'Error message' }`
51+
handler: (user) => {
52+
return user
53+
},
54+
55+
errors: {
56+
usernameOrPasswordMissing: 'Both username and password are required',
57+
usernameNotFound: 'Username ${username} not found',
58+
// For security reasons you may want to make this the same as the
59+
// usernameNotFound error so that a malicious user can't use the error
60+
// to narrow down if it's the username or password that's incorrect
61+
incorrectPassword: 'Incorrect password for ${username}',
62+
},
63+
64+
// How long a user will remain logged in, in seconds
65+
expires: 60 * 60 * 24 * 365 * 10,
66+
}
67+
68+
const resetPasswordOptions = {
69+
// handler() is invoked after the password has been successfully updated in
70+
// the database. Returning anything truthy will automatically log the user
71+
// in. Return `false` otherwise, and in the Reset Password page redirect the
72+
// user to the login page.
73+
handler: (_user) => {
74+
return true
75+
},
76+
77+
// If `false` then the new password MUST be different from the current one
78+
allowReusedPassword: true,
79+
80+
errors: {
81+
// the resetToken is valid, but expired
82+
resetTokenExpired: 'resetToken is expired',
83+
// no user was found with the given resetToken
84+
resetTokenInvalid: 'resetToken is invalid',
85+
// the resetToken was not present in the URL
86+
resetTokenRequired: 'resetToken is required',
87+
// new password is the same as the old password (apparently they did not forget it)
88+
reusedPassword: 'Must choose a new password',
89+
},
90+
}
91+
92+
const signupOptions = {
93+
// Whatever you want to happen to your data on new user signup. Redwood will
94+
// check for duplicate usernames before calling this handler. At a minimum
95+
// you need to save the `username`, `hashedPassword` and `salt` to your
96+
// user table. `userAttributes` contains any additional object members that
97+
// were included in the object given to the `signUp()` function you got
98+
// from `useAuth()`.
99+
//
100+
// If you want the user to be immediately logged in, return the user that
101+
// was created.
102+
//
103+
// If this handler throws an error, it will be returned by the `signUp()`
104+
// function in the form of: `{ error: 'Error message' }`.
105+
//
106+
// If this returns anything else, it will be returned by the
107+
// `signUp()` function in the form of: `{ message: 'String here' }`.
108+
handler: ({ username, hashedPassword, salt, userAttributes }) => {
109+
return db.user.create({
110+
data: {
111+
email: username,
112+
hashedPassword: hashedPassword,
113+
salt: salt,
114+
fullName: userAttributes['full-name'],
115+
},
116+
})
117+
},
118+
119+
// Include any format checks for password here. Return `true` if the
120+
// password is valid, otherwise throw a `PasswordValidationError`.
121+
// Import the error along with `DbAuthHandler` from `@redwoodjs/api` above.
122+
passwordValidation: (_password) => {
123+
return true
124+
},
125+
126+
errors: {
127+
// `field` will be either "username" or "password"
128+
fieldMissing: '${field} is required',
129+
usernameTaken: 'Username `${username}` already in use',
130+
},
131+
}
132+
133+
const authHandler = new DbAuthHandler(event, context, {
134+
// Provide prisma db client
135+
db: db,
136+
137+
// The name of the property you'd call on `db` to access your user table.
138+
// i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user`
139+
authModelAccessor: 'user',
140+
141+
// A map of what dbAuth calls a field to what your database calls it.
142+
// `id` is whatever column you use to uniquely identify a user (probably
143+
// something like `id` or `userId` or even `email`)
144+
authFields: {
145+
id: 'id',
146+
username: 'email',
147+
hashedPassword: 'hashedPassword',
148+
salt: 'salt',
149+
resetToken: 'resetToken',
150+
resetTokenExpiresAt: 'resetTokenExpiresAt',
151+
},
152+
153+
// Specifies attributes on the cookie that dbAuth sets in order to remember
154+
// who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
155+
cookie: {
156+
HttpOnly: true,
157+
Path: '/',
158+
SameSite: 'Strict',
159+
Secure: process.env.NODE_ENV !== 'development',
160+
161+
// If you need to allow other domains (besides the api side) access to
162+
// the dbAuth session cookie:
163+
// Domain: 'example.com',
164+
},
165+
166+
forgotPassword: forgotPasswordOptions,
167+
login: loginOptions,
168+
resetPassword: resetPasswordOptions,
169+
signup: signupOptions,
170+
})
171+
172+
return await authHandler.invoke()
173+
}

0 commit comments

Comments
 (0)