Skip to content

Commit

Permalink
finish all the instructions
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Sep 18, 2023
1 parent df41b5e commit 8fc39f1
Show file tree
Hide file tree
Showing 38 changed files with 338 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function Example() {
))}

{/* Setup a button that can append a new row with optional default value */}
<button {...list.append(items.name, { defaultValue: '' })}>add</button>
<button {...list.insert(items.name, { defaultValue: '' })}>add</button>
</fieldset>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function Example() {
))}

{/* Setup a button that can append a new row with optional default value */}
<button {...list.append(items.name, { defaultValue: '' })}>add</button>
<button {...list.insert(items.name, { defaultValue: '' })}>add</button>
</fieldset>
)
}
Expand Down Expand Up @@ -79,4 +79,4 @@ Let's add a "❌" button to delete images and a "➕ Image" button to add new
images.

- [📜 Conform Intent Button](https://conform.guide/intent-button)
- [📜 Conform Nested List](https://conform.guide/complex-structures#nested-list)
- [📜 Conform Nested Array](https://conform.guide/complex-structures#nested-array)
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export default function NoteEdit() {
))}
</ul>
</div>
{/* 🐨 add a button here with list.append and fields.images.name to add another image */}
{/* 🐨 add a button here with list.insert and fields.images.name to add another image */}
{/* 💰 you'll want to set the defaultValue to "{}" (otherwise it'll default to null which is invalid according to our schema) */}
{/* 🐨 you can use "➕ Image" as the button text */}
{/* 💯 if you have extra time, consider the screen reader experience. How could you make it better? */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ implemented in my solution:

<CodeFile
file="app/routes/users+/$username_+/notes.$noteId_.edit.tsx"
range="186-192,198-204"
highlight="190-191,202-203"
range="180-186,192-198"
highlight="184-185,196-197"
buttons="solution"
/>

</details>

🧝‍♂️ I'm going to make a new <LinkToApp to="/signup" /> route and link to it from
the <InlineFile file="app/root.tsx" /> file for the next exercise. It'll just be
a form that takes an "email" input. Feel free to put that together if you have
extra time, or you can <DiffLink>check my work</DiffLink>.
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
2 changes: 1 addition & 1 deletion exercises/05.complex-structures/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export default function Todos() {
))}
</ul>
<div>
<button {...list.append(tasks.name)}>Add task</button>
<button {...list.insert(tasks.name)}>Add task</button>
</div>
<button>Save</button>
</form>
Expand Down
17 changes: 17 additions & 0 deletions exercises/06.honeypot/01.problem.basic/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
# Basic Honeypot

👨‍💼 When users sign up for an account on our app, we send a confirmation email.
If spam bots submit random people's email addresses in there, we'll get marked
as spam and our deliverability will be poor (emails we send won't get to where
they're supposed to go).

Kellie 🧝‍♂️ put together a new <LinkToApp to="/signup" /> route which accepts the
user's email address. We need you to add a honeypot field to the form so we can
detect bots and not send emails if that field is filled out.

You just need to make sure that regular users don't accidentally fill the field
out. It can be pretty basic, because many bots aren't very sophisticated. But
we'll improve it later to deal with more sophisticated bots in the future.

The form only actually redirects to `/` for right now. If the honeypot field is
filled in then the `action` should return a 400 error response (you can use
`invariantResponse` for this if you like).
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
4 changes: 4 additions & 0 deletions exercises/06.honeypot/01.solution.basic/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Basic Honeypot

👨‍💼 Great job! Your honeypot is awesome, but Kellie 🧝‍♂️ just found a library that
will do this for us, so we're going to use that library instead. Sorry about
that 😅
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
45 changes: 45 additions & 0 deletions exercises/06.honeypot/02.problem.util/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,46 @@
# Remix Utils

👨‍💼 We're going to use [`remix-utils`](https://npm.im/remix-utils) to put
together the honeypot in the UI and check it in our `action`. This is going to
require a bit of setup because it can do more than just handle the single field
for us.

First you're going to need to
create <InlineFile file="app/utils/honeypot.server.ts" /> to create a honeypot
instance. For example:

```tsx nocopy
import { Honeypot } from 'remix-utils/honeypot/server'

export const honeypot = new Honeypot({
validFromFieldName: 'validFrom',
encryptionSeed: process.env.HONEY_POT_ENCRYPTION_SEED,
nameFieldName: 'name',
randomizeNameFieldName: true,
})
```

We don't need all those options just yet though. In fact, for this first bit, we
just want to set `validFromFieldName` to `null` for now. We'll get to the rest
of the options later.

Once you have that set up, you can use it in the
`action` of <InlineFile file="app/routes/_auth+/signup.tsx" />. For example:

```tsx nocopy
try {
honeypot.check(formData)
} catch (error) {
if (error instanceof SpamError) {
throw new Response('Form not submitted properly', { status: 400 })
}
throw error
}
```

The `SpamError` comes from `remix-utils/honeypot/server`.

And then for the UI, you can remove our custom `input` stuff with
`<HoneypotInputs />` from `remix-utils/honeypot/react`.

Good luck!
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
9 changes: 9 additions & 0 deletions exercises/06.honeypot/02.solution.util/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
# Remix Utils

👨‍💼 Great! That's better. It's nice to use a library for stuff like this because
it means that as spam bots get more sophisticated, the library can be steadily
improved and we can just update our code to use the new version.

🧝‍♂️ I'm going to update <InlineFile file="app/root.tsx" /> a bit to make the next
thing you're going to do a bit easier. It's easier to
just show you the diff than explain it. So you
can <DiffLink>check it out here</DiffLink>.
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
23 changes: 23 additions & 0 deletions exercises/06.honeypot/03.problem.provider/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# Honeypot Provider

👨‍💼 If a user is able to submit the form within a second of the form being
generated, they're probably not a human (or maybe they're just
[Barry Allen](https://en.wikipedia.org/wiki/List_of_The_Flash_characters#Barry_Allen_/_Flash) ⚡).
So as a part of our honeypot, we can have another hidden field that keeps track
of when the form was generated. Then when the form is submitted, we just make
sure it was submitted at least a second after it was generated.

There are a few problems with this we'll need to consider. For example, if we're
running automated tests, then our user actually _is_ a bot and that's okay 😅
So when we're running tests, we don't want to include the valid from field.

We'll know whether we're in a testing environment if `process.env.TESTING` is
set. (That's set in <InlineFile file="index.js" />). If that's set, then just
set the `validFromFieldName` to `null` and that will prevent remix-utils from
including and checking for that field. Otherwise, you can set it to a string, or
just use `undefined` to have the default value be used.

Another challenge will be is synchronizing our UI with our server config for the
honeypot fields. So we need to update <InlineFile file="app/root.tsx" /> to
handle this.

There are other issues, but let's just start with this.
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
13 changes: 13 additions & 0 deletions exercises/06.honeypot/03.solution.provider/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
# Honeypot Provider

👨‍💼 Great! Now we're probably going to catch even more bots! But we've got
another problem we'll want to deal with next.

🧝‍♂️ Before you get to that though, I'm going to make a small change. We're going
to apply this honeypot stuff to all our public forms, so to reduce repetition,
I'm going to move some of the boilerplate from the `action`
in <InlineFile file="app/routes/_auth+/signup.tsx" />
to a utility called `checkHoneypot`
in <InlineFile file="app/utils/honeypot.server.ts" />.

Feel free to <DiffLink>check the diff</DiffLink> or even do it yourself if you
like.
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
30 changes: 30 additions & 0 deletions exercises/06.honeypot/04.problem.seed/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
# Encryption Seed

👨‍💼 So, you may not have noticed this, but `remix-utils` doesn't actually put the
real date the form was generated in the `validFrom` input of the form. It's just
some random string of characters. Well, it's not really random, it's actually
encrypted. The reason for this is because we don't want bots to be able to just
change the date on the form and then submit it quickly. So `remix-utils` will
encrypt the actual valid date and that's what the form is set to.

To be able to decrypt the value, we need an encryption key. `remix-utils` will
generate one for us if we don't set one ourselves. Unfortunately, there's a
problem with doing things this way. The key is generated at startup time, so if
you restart your server, or your running multiple instances of your app, a form
could be generated with one key and validated with another.

So instead, we can set it to something consistent across all instances of our
app. We can do this by setting the `encryptionSeed` option in our config. The
tricky bit is we need this to be secret, so we're not going to just commit this
to the repository. We need this to be kept secret. So we'll use environment
variables.

So we're going to place it in our `.gitignore`d `.env` file which we're loading
at startup time during development, and then you'll want to make sure to set
this environment variable in your production environment as well
([for example](https://fly.io/docs/reference/secrets/)).

🐨 So first, you'll set the variable in <InlineFile file=".env" />, then
go to <InlineFile file="app/utils/env.server.ts" /> to validate at startup time
that the variable is set (and get type safety on it as well).

🐨 Once you've got that, you can set the `encryptionSeed` in the honeypot config.
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
5 changes: 5 additions & 0 deletions exercises/06.honeypot/04.solution.seed/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
# Encryption Seed

👨‍💼 Awesome! Now we're able to handle the massive network of bots randomly
submitting forms all over the internet. Again, this won't really help with a
targeted attack (we'll do more with that later). This definitely helps a lot
with general abuse though, so great work!
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
45 changes: 45 additions & 0 deletions exercises/07.csrf/01.problem.setup/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,46 @@
# CSRF Setup

👨‍💼 We've got to get some things set up with the `remix-utils` CSRF utilities
before we can actually start protecting our forms. We'll dive deeper into
cookies in the [Web Auth](https://auth.epicweb.dev) workshop later, but you do
need to set a signed cookie in the user's browser so you can use
[Remix's `createCookie`](https://remix.run/docs/en/main/utils/cookies#createcookie)
to help with that.

```tsx
import { createCookie } from '@remix-run/node'

const cookie = createCookie('csrf', {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
secrets: process.env.SESSION_SECRET.split(','),
})
```

🐨 Go ahead and stick that in <InlineFile file="app/utils/csrf.server.ts" />.

You'll notice we're using another environment variable. 🐨 You'll need to set
that up like we did with the `HONEYPOT_SECRET` earlier.

Feel free to [read the Remix docs](https://remix.run/docs/en/main/utils/cookies)
all about cookies if you want to learn more about these options, but we'll get
to this stuff in more depth in the Web Auth workshop.

🐨 Once you have the cookie object created, you can use that to create a CSRF
utility;

```tsx
import { CSRF } from 'remix-utils/csrf/server'

// ...

export const csrf = new CSRF({ cookie })
```

Now, we need to get the user's unique token in our UI for our forms and in the
user's cookie so we can validate it on the server. We'll do that
in <InlineFile file="app/root.tsx" />. 🐨 See you there!

- [📜 Remix `createCookie`](https://remix.run/docs/en/main/utils/cookies#createcookie)
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
25 changes: 25 additions & 0 deletions exercises/07.csrf/01.solution.setup/README.mdx
Original file line number Diff line number Diff line change
@@ -1 +1,26 @@
# CSRF Setup

👨‍💼 Great! You should now be able to get that cookie value set in the browser's
"cookie jar." You can verify this by opening the browser's developer tools and
looking at the cookies for the page under the "Application" tab. The cookie
will be called "csrf" and have a value that looks like a bunch of nonsense like:

```
IjdDa3p6WkV1d3NNel&nev3r&60nna&gIv3&yoU&uP&UW0zYmxDMDNyRjQi.NM71601wmCvZ%2FZaGIG6wV%2FuX%2FvGafzDEAmamK1hNu88
```

And while you can't visually verify it, that (unsigned) token value is in the
browser as well. You can check by executing this in the console:

```js
__remixContext.state.loaderData.root.csrfToken
```

Which should get you something like this:

```
7Ck&Nev3r&60nna&1et&yoU&d0wn&UOfQm3blC03rF4
```

So the next step is to get that into our form and verify it to prove that the
form was submitted using our app and not some other site. Let's go!
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default function NoteEdit() {
</div>
<Button
className="mt-3"
{...list.append(fields.images.name, { defaultValue: {} })}
{...list.insert(fields.images.name, { defaultValue: {} })}
>
<span aria-hidden>➕ Image</span>{' '}
<span className="sr-only">Add image</span>
Expand Down
Loading

0 comments on commit 8fc39f1

Please sign in to comment.