Skip to content

Commit a777e20

Browse files
committed
ErrorBoundary, Loader, Redirects work
How to set up routes in Remix. How to build a sign in and sign up form with validation. How session-based authentication works. How to protect privates routes by implementing authorization. How to store and query your data using Prisma when creating and authenticating users.
1 parent 858b7a8 commit a777e20

File tree

4 files changed

+131
-22
lines changed

4 files changed

+131
-22
lines changed

kudos/app/components/FormField.tsx

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { useEffect, useState } from "react";
2+
13
interface FormFieldProps {
24
htmlFor: string;
35
label: string;
46
type?: string;
57
value: any;
6-
error: string;
8+
error?: string;
79
onChange?: (...args: any) => any;
810
}
911

@@ -12,21 +14,35 @@ const FormField = ({
1214
label,
1315
type = `text`,
1416
value,
17+
error = "",
1518
onChange = () => null,
16-
}: FormFieldProps) => (
17-
<>
18-
<label htmlFor={htmlFor} className="text-blue-600 font-semibold">
19-
{label}
20-
</label>
21-
<input
22-
onChange={onChange}
23-
type={type}
24-
id={htmlFor}
25-
name={htmlFor}
26-
className="w-full p-2 rounded-xl my-2"
27-
value={value}
28-
/>
29-
</>
30-
);
19+
}: FormFieldProps) => {
20+
const [errorText, setErrorText] = useState(error);
21+
useEffect(() => {
22+
setErrorText(error);
23+
}, [error]);
24+
25+
return (
26+
<>
27+
<label htmlFor={htmlFor} className="text-blue-600 font-semibold">
28+
{label}
29+
</label>
30+
<input
31+
onChange={(e) => {
32+
onChange(e);
33+
setErrorText("");
34+
}}
35+
type={type}
36+
id={htmlFor}
37+
name={htmlFor}
38+
className="w-full p-2 rounded-xl my-2"
39+
value={value}
40+
/>
41+
<div className="text-xs font-semibold text-center tracking-wide text-red-500 w-full">
42+
{errorText || ""}
43+
</div>
44+
</>
45+
);
46+
};
3147

3248
export { FormField };

kudos/app/routes/index.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { requireUserId } from "~/utils/auth.server";
2+
3+
import type { LoaderFunction } from "@remix-run/node";
4+
15
export function ErrorBoundary({ error }: any) {
26
console.log(error);
37
return (
@@ -8,6 +12,11 @@ export function ErrorBoundary({ error }: any) {
812
);
913
}
1014

15+
const loader: LoaderFunction = async ({ request }) => {
16+
await requireUserId(request);
17+
return null;
18+
};
19+
1120
export default function Index() {
1221
// const todos = useLoaderData();
1322
return (
@@ -18,3 +27,5 @@ export default function Index() {
1827
</div>
1928
);
2029
}
30+
31+
export { loader };

kudos/app/routes/login.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useState } from "react";
2-
import { json } from "@remix-run/node";
1+
import { useState, useEffect, useRef } from "react";
2+
import { json, redirect } from "@remix-run/node";
33
import { useActionData } from "@remix-run/react";
44

55
import { FormField } from "~/components/FormField";
@@ -9,9 +9,13 @@ import {
99
validatePassword,
1010
validateName,
1111
} from "~/utils/validators.server";
12-
import { login, register } from "~/utils/auth.server";
12+
import { login, register, getUser } from "~/utils/auth.server";
1313

14-
import type { ActionFunction } from "@remix-run/node";
14+
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
15+
16+
const loader: LoaderFunction = async ({ request }) => {
17+
return (await getUser(request)) ? redirect(`/`) : null;
18+
};
1519

1620
const action: ActionFunction = async ({ request }) => {
1721
const form = await request.formData();
@@ -77,6 +81,7 @@ const action: ActionFunction = async ({ request }) => {
7781

7882
export default function Login() {
7983
const actionData = useActionData();
84+
const firstLoad = useRef(true);
8085
const [errors, setErrors] = useState(actionData?.errors || {});
8186
const [formError, setFormError] = useState(actionData?.error || "");
8287
const [action, setAction] = useState("login");
@@ -87,6 +92,33 @@ export default function Login() {
8792
lastName: actionData?.fields?.firstName || "",
8893
});
8994

95+
/*
96+
If the user is shown an error and switches forms, you will need to clear out the form and any errors being shown. Use these effects to achieve this:
97+
*/
98+
useEffect(() => {
99+
if (!firstLoad.current) {
100+
const newState = {
101+
email: "",
102+
password: "",
103+
firstName: "",
104+
lastName: "",
105+
};
106+
setErrors(newState);
107+
setFormError("");
108+
setFormData(newState);
109+
}
110+
}, [action]);
111+
112+
useEffect(() => {
113+
if (!firstLoad.current) {
114+
setFormError("");
115+
}
116+
}, [formData]);
117+
118+
useEffect(() => {
119+
firstLoad.current = false;
120+
}, []);
121+
90122
// Updates the form data when an input changes
91123
const handleInputChange = (
92124
event: React.ChangeEvent<HTMLInputElement>,
@@ -166,4 +198,4 @@ export default function Login() {
166198
);
167199
}
168200

169-
export { action };
201+
export { action, loader };

kudos/app/utils/auth.server.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,54 @@ const login = async ({ email, password }: LoginForm) => {
6666
return createUserSession(user.id, `/`);
6767
};
6868

69-
export { createUserSession, register, login };
69+
const logout = async (request: Request) => {
70+
const session = await getUserSession(request);
71+
return redirect("/login", {
72+
headers: {
73+
"Set-Cookie": await storage.destroySession(session),
74+
},
75+
});
76+
};
77+
78+
const getUserId = async (request: Request) => {
79+
const session = await getUserSession(request);
80+
const userId = session.get("userId");
81+
if (!userId || typeof userId !== "string") return null;
82+
return userId;
83+
};
84+
85+
const getUserSession = (request: Request) => {
86+
return storage.getSession(request.headers.get("Cookie"));
87+
};
88+
89+
const requireUserId = async (
90+
request: Request,
91+
redirectTo: string = new URL(request.url).pathname
92+
) => {
93+
const session = await getUserSession(request);
94+
const userId = session.get("userId");
95+
if (!userId || typeof userId !== "string") {
96+
const searchParams = new URLSearchParams([["redirectTo", redirectTo]]);
97+
throw redirect(`/login?${searchParams}`);
98+
}
99+
return userId;
100+
};
101+
102+
const getUser = async (request: Request) => {
103+
const userId = await getUserId(request);
104+
if (typeof userId !== "string") {
105+
return null;
106+
}
107+
108+
try {
109+
const user = await prisma.user.findUnique({
110+
where: { id: userId },
111+
select: { id: true, email: true, profile: true },
112+
});
113+
return user;
114+
} catch {
115+
throw logout(request);
116+
}
117+
};
118+
119+
export { createUserSession, register, login, requireUserId, getUser };

0 commit comments

Comments
 (0)