Skip to content

Commit d294304

Browse files
committed
finishing auth lesson and exercise for remix
1 parent 2d8eef4 commit d294304

File tree

11 files changed

+426
-14
lines changed

11 files changed

+426
-14
lines changed

remix/lessons/05-forms-and-actions/exercise/GUIDE.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
# Forms and Fetcher
2-
3-
The goal of this exercise is to practice using forms and formData with Remix
1+
# Forms and Actions
42

53
First, review the loader of `root.tsx`. This loader gets the cart data now. Before in the lecture, the cart data was loaded down lower in the loader of `_products-layout._index.tsx`. By "lifting" the loader data, we have access to the cart in the entire UI of our app, not just in that page. When you are able to add items to the cart, you'll notice the "Cart is Empty" message start to change.
64

remix/lessons/06-authentication/exercise-final/components/MainLayout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ function SubHeader() {
4343
<NavLink to="/login" className="inline-block py-3 px-5 -mb-[1px] border-b-2">
4444
Login
4545
</NavLink>
46+
<NavLink to="/register" className="inline-block py-3 px-5 -mb-[1px] border-b-2">
47+
Register
48+
</NavLink>
4649
</nav>
4750
</CenterContent>
4851
)
Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
import { LoaderArgs, json } from '@remix-run/node'
22
import { useLoaderData } from '@remix-run/react'
3-
import { storage } from '../utils/auth.server'
3+
import { getSessionUser } from '../utils/auth.server'
44

55
export const loader = async ({ request }: LoaderArgs) => {
6-
// Get the session from the cookie
7-
const session = await storage.getSession(request.headers.get('Cookie'))
8-
9-
// Get the userId from the session if it exists
10-
let userId = session.get('userId') as string | undefined
11-
12-
return json({ userId: userId !== undefined && parseInt(userId) })
6+
const user = await getSessionUser(request)
7+
return json({ user })
138
}
149

1510
export default function () {
16-
const { userId } = useLoaderData<typeof loader>()
17-
return userId ? <div>User ID is {userId}</div> : <div>User is not logged in</div>
11+
const { user } = useLoaderData<typeof loader>()
12+
return user ? <div>User ID is {user.id}</div> : <div>User is not logged in</div>
1813
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useState } from 'react'
2+
import * as z from 'zod'
3+
import { json, redirect } from '@remix-run/node'
4+
import { Form, useActionData } from '@remix-run/react'
5+
import { FieldWrap } from '~/components/FormFields'
6+
import { Heading } from '~/components/Heading'
7+
import type { ActionArgs } from '@remix-run/node'
8+
import { createUserSession, registerUser, verifyUser } from '../utils/auth.server'
9+
import { usernameExists } from '~/utils/db.server'
10+
11+
const formSchema = z.object({
12+
username: z.string().min(5, { message: 'Must be at least 5 characters' }),
13+
password: z.string().min(5, { message: 'Must be at least 5 characters' }),
14+
})
15+
16+
type FormDataType = z.infer<typeof formSchema>
17+
type FormErrorType = {
18+
[k in keyof FormDataType]?: string[] | undefined
19+
}
20+
21+
export async function action({ request }: ActionArgs) {
22+
const formData = await request.formData()
23+
const formValues = Object.fromEntries(formData)
24+
const results = formSchema.safeParse(formValues)
25+
if (!results.success) return json({ error: 'Invalid Data' }, { status: 400 })
26+
27+
const { username, password } = results.data
28+
const userExists = await usernameExists(username)
29+
if (userExists) return json({ error: 'Username already registered' }, { status: 400 })
30+
31+
const userId = await registerUser(username, password)
32+
if (!userId) return json({ error: 'We were not able to register this user' }, { status: 400 })
33+
34+
return createUserSession(userId, '/')
35+
}
36+
37+
export default function Login() {
38+
const [formErrors, setFormErrors] = useState<FormErrorType>()
39+
const { error } = useActionData<typeof action>() || {}
40+
41+
function onSubmit(event: React.FormEvent<HTMLFormElement>) {
42+
const formValues = Object.fromEntries(new FormData(event.currentTarget))
43+
const results = formSchema.safeParse(formValues)
44+
if (!results.success) {
45+
event.preventDefault()
46+
setFormErrors(results.error.flatten().fieldErrors)
47+
}
48+
}
49+
50+
return (
51+
<div className="ml-auto mr-auto max-w-[600px]">
52+
<div className="bg-white rounded-md shadow-md p-6 space-y-6">
53+
<Heading size={4}>Register</Heading>
54+
{error && <div className="notice error">{error}</div>}
55+
<Form onSubmit={onSubmit} method="post" className="space-y-3" autoComplete="off">
56+
<FieldWrap label="Username" required errors={formErrors?.username}>
57+
{(field) => <input {...field} className="form-field" type="text" name="username" />}
58+
</FieldWrap>
59+
<FieldWrap label="Password" required errors={formErrors?.password}>
60+
{(field) => <input {...field} className="form-field" type="password" name="password" />}
61+
</FieldWrap>
62+
<footer>
63+
<button type="submit" className="button">
64+
Register
65+
</button>
66+
</footer>
67+
</Form>
68+
</div>
69+
</div>
70+
)
71+
}

remix/lessons/06-authentication/exercise-final/utils/auth.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createCookieSessionStorage, redirect } from '@remix-run/node'
2-
import { createUser, getUser, getUserPasswordHash } from './db.server'
2+
import { createUser, getUser, getUserPasswordHash } from '~/utils/db.server'
33
import bcrypt from 'bcryptjs'
44

55
/****************************************
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Forms and Fetcher
2+
3+
First, review `utils/auth.server.ts` where we have lots of utility functions to help you manage your sessions
4+
5+
# Task 1
6+
7+
Work on the home page: `routes/_index.tsx` and get the message to correctly show the user's login status. It currently is hard-coded to always say the user is not logged in.
8+
9+
You can use the real login page to log into a user with:
10+
11+
username: admin
12+
password: admin
13+
14+
If you need to remove a login session, you can do so in Chrome DevTools under Application > Cookies and remove the `react_training_remix_auth` cookie.
15+
16+
# Task 2
17+
18+
Copy the login page and all it's code to make a `routes/register.tsx` page. Almost all the code is the same but you'll probably want to update the header and button label to say "Register" instead of "Login".
19+
20+
The only real difference between login and register will be in the action. Instead of verifying a user before you create the user session, you'll see if the username exists already with `usernameExists(username)`. If it does you should respond with a 400 status code. Then you use `registerUser(username, password)` to register the user and then create the session. Both of those functions can be found in the `auth.server.ts` file.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { NavLink } from '@remix-run/react'
2+
import { SelectedLesson } from '~/state/LessonContext'
3+
import { Logo } from '~/components/Logo'
4+
5+
type MainLayoutProps = {
6+
children: React.ReactNode
7+
}
8+
9+
export function MainLayout({ children }: MainLayoutProps) {
10+
return (
11+
<div>
12+
<Header />
13+
<SubHeader />
14+
<CenterContent className="pt-6 pb-20">{children}</CenterContent>
15+
</div>
16+
)
17+
}
18+
19+
function Header() {
20+
return (
21+
<header className="d bg-gradient-to-r from-sky-400 to-indigo-950">
22+
<CenterContent className="border-b py-3">
23+
<div className="flex justify-between items-center">
24+
<div className="">
25+
<Logo />
26+
</div>
27+
<div className="text-white/60">
28+
<SelectedLesson />
29+
</div>
30+
</div>
31+
</CenterContent>
32+
</header>
33+
)
34+
}
35+
36+
function SubHeader() {
37+
return (
38+
<CenterContent className="bg-white border-b">
39+
<nav className="primary-nav">
40+
<NavLink to="/" className="inline-block py-3 px-5 -mb-[1px] border-b-2">
41+
Home
42+
</NavLink>
43+
<NavLink to="/login" className="inline-block py-3 px-5 -mb-[1px] border-b-2">
44+
Login
45+
</NavLink>
46+
<NavLink to="/register" className="inline-block py-3 px-5 -mb-[1px] border-b-2">
47+
Register
48+
</NavLink>
49+
</nav>
50+
</CenterContent>
51+
)
52+
}
53+
54+
type CenterContentProps = {
55+
className?: string
56+
children: React.ReactNode
57+
}
58+
59+
export function CenterContent({ children, className }: CenterContentProps) {
60+
return (
61+
<div className={className}>
62+
<div className="ml-auto mr-auto max-w-[1200px] pl-3 pr-3">{children}</div>
63+
</div>
64+
)
65+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { PropsWithChildren } from 'react'
2+
import {
3+
Links,
4+
LiveReload,
5+
Meta,
6+
Outlet,
7+
Scripts,
8+
ScrollRestoration,
9+
isRouteErrorResponse,
10+
useLoaderData,
11+
useRouteError,
12+
} from '@remix-run/react'
13+
import { type LinksFunction, json } from '@remix-run/node'
14+
import stylesheet from '~/styles/app.css'
15+
import { MainLayout } from './components/MainLayout'
16+
import { LessonProvider } from '~/state/LessonContext'
17+
import { CenterContent } from '~/components/CenterContent'
18+
import { Heading } from '~/components/Heading'
19+
20+
export const links: LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }]
21+
22+
export async function loader() {
23+
const lesson = process.env.REMIX_APP_DIR?.split('/').slice(-2).join('/') || ''
24+
return json({ lesson })
25+
}
26+
27+
export default function App() {
28+
const { lesson } = useLoaderData<typeof loader>()
29+
30+
return (
31+
<Document>
32+
<LessonProvider selectedLesson={lesson}>
33+
<MainLayout>
34+
<Outlet />
35+
</MainLayout>
36+
</LessonProvider>
37+
</Document>
38+
)
39+
}
40+
41+
export function ErrorBoundary() {
42+
const error = useRouteError()
43+
44+
let heading = 'Unknown Error'
45+
let message = ''
46+
47+
if (isRouteErrorResponse(error)) {
48+
heading = error.status + ' ' + error.statusText
49+
message = error.data
50+
} else if (error instanceof Error) {
51+
heading = 'Page Error'
52+
message = error.message
53+
}
54+
55+
return (
56+
<Document>
57+
<CenterContent className="pt-6 pb-20">
58+
<div className="bg-white p-6 rounded-md space-y-6">
59+
<Heading size={1}>{heading}</Heading>
60+
<p>{message}</p>
61+
</div>
62+
</CenterContent>
63+
</Document>
64+
)
65+
}
66+
67+
export function Document({ children }: PropsWithChildren) {
68+
return (
69+
<html lang="en">
70+
<head>
71+
<meta charSet="utf-8" />
72+
<meta name="viewport" content="width=device-width,initial-scale=1" />
73+
<Meta />
74+
<Links />
75+
<link rel="preconnect" href="https://fonts.googleapis.com" />
76+
<link
77+
href="https://fonts.googleapis.com/css2?&family=Inter:wght@400;700&display=swap"
78+
rel="stylesheet"
79+
/>
80+
</head>
81+
<body>
82+
{children}
83+
<ScrollRestoration />
84+
<Scripts />
85+
<LiveReload />
86+
</body>
87+
</html>
88+
)
89+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { LoaderArgs, json } from '@remix-run/node'
2+
import { useLoaderData } from '@remix-run/react'
3+
// import { ??? } from '../utils/auth.server'
4+
5+
export const loader = async ({ request }: LoaderArgs) => {
6+
// Get the session user. Look in ../utils/auth.server
7+
return null
8+
}
9+
10+
export default function () {
11+
const user = false // Use useLoaderData instead:
12+
// const { user } = useLoaderData<typeof loader>()
13+
return user ? <div>User ID is {user.id}</div> : <div>User is not logged in</div>
14+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useState } from 'react'
2+
import * as z from 'zod'
3+
import { json, redirect } from '@remix-run/node'
4+
import { Form, useActionData } from '@remix-run/react'
5+
import { FieldWrap } from '~/components/FormFields'
6+
import { Heading } from '~/components/Heading'
7+
import type { ActionArgs } from '@remix-run/node'
8+
import { createUserSession, verifyUser } from '../utils/auth.server'
9+
10+
const formSchema = z.object({
11+
username: z.string().min(5, { message: 'Must be at least 5 characters' }),
12+
password: z.string().min(5, { message: 'Must be at least 5 characters' }),
13+
})
14+
15+
type FormDataType = z.infer<typeof formSchema>
16+
type FormErrorType = {
17+
[k in keyof FormDataType]?: string[] | undefined
18+
}
19+
20+
export async function action({ request }: ActionArgs) {
21+
const formData = await request.formData()
22+
const formValues = Object.fromEntries(formData)
23+
const results = formSchema.safeParse(formValues)
24+
if (!results.success) return json({ error: 'Invalid Data' }, { status: 400 })
25+
26+
const { username, password } = results.data
27+
const userId = await verifyUser(username, password)
28+
if (!userId) return json({ error: 'User not found' }, { status: 400 })
29+
30+
const redirectPath = '/'
31+
return createUserSession(userId, redirectPath)
32+
}
33+
34+
export default function Login() {
35+
const [formErrors, setFormErrors] = useState<FormErrorType>()
36+
const { error } = useActionData<typeof action>() || {}
37+
38+
function onSubmit(event: React.FormEvent<HTMLFormElement>) {
39+
const formValues = Object.fromEntries(new FormData(event.currentTarget))
40+
const results = formSchema.safeParse(formValues)
41+
if (!results.success) {
42+
event.preventDefault()
43+
setFormErrors(results.error.flatten().fieldErrors)
44+
}
45+
}
46+
47+
return (
48+
<div className="ml-auto mr-auto max-w-[600px]">
49+
<div className="bg-white rounded-md shadow-md p-6 space-y-6">
50+
<Heading size={4}>Login</Heading>
51+
{error && <div className="notice error">{error}</div>}
52+
<Form onSubmit={onSubmit} method="post" className="space-y-3" autoComplete="off">
53+
<FieldWrap label="Username" required errors={formErrors?.username}>
54+
{(field) => <input {...field} className="form-field" type="text" name="username" />}
55+
</FieldWrap>
56+
<FieldWrap label="Password" required errors={formErrors?.password}>
57+
{(field) => <input {...field} className="form-field" type="password" name="password" />}
58+
</FieldWrap>
59+
<footer>
60+
<button type="submit" className="button">
61+
Login
62+
</button>
63+
</footer>
64+
</Form>
65+
</div>
66+
</div>
67+
)
68+
}

0 commit comments

Comments
 (0)