Skip to content

Commit 490a51b

Browse files
authored
chore: add authentication and protected routes with AWS Amplify to todo-app (#50)
* chore: add authentication and protected routes with AWS Amplify to todo-app * chore: update authorization methods in resource schema to use owner-based access * feat: add signOut button * docs: update README for clarity and additional information
1 parent d4239d9 commit 490a51b

File tree

10 files changed

+906
-230
lines changed

10 files changed

+906
-230
lines changed

examples/todo-app/README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
# Todo app using React Router SSR and AWS Amplify
22

3-
A Todo application using React Router and AWS Amplify.
3+
A Todo application using React Router Framework and AWS Amplify.
4+
5+
## About this Todo App
6+
7+
This Todo app is a simple Todo application built with React Router and AWS Amplify Auth and Data. It uses server-side rendering (SSR) to improve performance and SEO.

examples/todo-app/amplify/data/resource.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ const schema = a.schema({
55
content: a.string(),
66
isDone: a.boolean()
77
})
8-
.authorization(allow => [allow.publicApiKey()])
8+
.authorization(allow => [allow.owner()])
99
});
1010

1111
export type Schema = ClientSchema<typeof schema>;
1212
export const data = defineData({
1313
schema,
1414
authorizationModes: {
15-
defaultAuthorizationMode: 'apiKey',
15+
defaultAuthorizationMode: 'userPool',
1616
}
1717
});
1818

examples/todo-app/app/root.tsx

+20-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import {
55
Outlet,
66
Scripts,
77
ScrollRestoration,
8+
useNavigate,
89
} from "react-router";
10+
import { useEffect } from "react";
11+
import { Hub } from "aws-amplify/utils";
12+
import { Authenticator, ThemeProvider } from "@aws-amplify/ui-react";
13+
import "@aws-amplify/ui-react/styles.css";
914

1015
import type { Route } from "./+types/root";
1116
import "./app.css";
@@ -38,7 +43,11 @@ export function Layout({ children }: { children: React.ReactNode }) {
3843
<Links />
3944
</head>
4045
<body>
41-
{children}
46+
<Authenticator.Provider>
47+
<ThemeProvider>
48+
{children}
49+
</ThemeProvider>
50+
</Authenticator.Provider>
4251
<ScrollRestoration />
4352
<Scripts />
4453
</body>
@@ -47,7 +56,16 @@ export function Layout({ children }: { children: React.ReactNode }) {
4756
}
4857

4958
export default function App() {
50-
return <Outlet />
59+
const navigate = useNavigate();
60+
useEffect(() => {
61+
Hub.listen("auth", (data) => {
62+
const { payload } = data;
63+
if (payload.event === "signedIn") {
64+
navigate("/");
65+
}
66+
});
67+
}, [navigate]);
68+
return <Outlet />;
5169
}
5270

5371
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {

examples/todo-app/app/routes.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { type RouteConfig, index, route } from "@react-router/dev/routes";
1+
import { type RouteConfig, index, route, layout } from "@react-router/dev/routes";
22

33
export default [
4-
index("./routes/index.tsx"),
5-
route("new", "./routes/new.tsx"),
6-
route(":todoId", "./routes/$todoId/index.tsx"),
7-
route(":todoId/edit", "./routes/$todoId/edit.tsx"),
4+
route("login", "./routes/auth/login.tsx"), // Update login route path
5+
layout("./routes/protected/layout.tsx", [ // Wrap protected routes
6+
index("./routes/index.tsx"),
7+
route("new", "./routes/new.tsx"),
8+
route(":todoId", "./routes/$todoId/index.tsx"),
9+
route(":todoId/edit", "./routes/$todoId/edit.tsx"),
10+
]),
811
] satisfies RouteConfig;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Authenticator } from "@aws-amplify/ui-react";
2+
import "@aws-amplify/ui-react/styles.css";
3+
import { getCurrentUser } from "aws-amplify/auth";
4+
import { redirect } from "react-router";
5+
import type { Route } from "./+types/login";
6+
7+
export const meta: Route.MetaFunction = () => {
8+
return [
9+
{ title: "Login - Todo App" },
10+
{ name: "description", content: "Login to Todo App" },
11+
];
12+
}
13+
14+
export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
15+
try {
16+
const user = await getCurrentUser();
17+
if (user) {
18+
return redirect("/");
19+
}
20+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
21+
} catch (error) {
22+
// ignore error - user not logged in
23+
}
24+
return {};
25+
}
26+
27+
export default function Login() {
28+
return (
29+
<div className="flex min-h-screen items-center justify-center p-4">
30+
<div className="w-full max-w-sm">
31+
<Authenticator />
32+
</div>
33+
</div>
34+
);
35+
}

examples/todo-app/app/routes/index.tsx

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { Route } from "./+types/index";
22
import { TodoList } from "~/components/TodoList";
33
import { runWithAmplifyServerContext } from "~/lib/amplifyServerUtils";
4-
import { data, Link } from "react-router";
4+
import { data, Link, useNavigate } from "react-router";
55
import { client } from "~/lib/amplify-ssr-client";
6+
import { signOut } from "aws-amplify/auth";
67

78
export function meta() {
89
return [
@@ -30,18 +31,31 @@ export async function loader({ request }: Route.LoaderArgs) {
3031

3132
export default function Home({ loaderData }: Route.ComponentProps) {
3233
const { todos } = loaderData;
34+
const navigate = useNavigate();
3335
return (
3436
<div className="flex flex-col gap-y-3 my-4 mx-2">
3537
<h1 className="text-2xl font-bold">Todo List</h1>
3638
<TodoList items={todos} />
37-
<Link to="/new">
39+
<div className="flex justify-between">
40+
<Link to="/new">
41+
<button
42+
type="button"
43+
className="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded"
44+
>
45+
Add Todo
46+
</button>
47+
</Link>
3848
<button
3949
type="button"
40-
className="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded"
50+
className="bg-transparent hover:bg-red-500 text-red-700 font-semibold hover:text-white py-2 px-4 border border-red-500 hover:border-transparent rounded"
51+
onClick={async () => {
52+
await signOut();
53+
await navigate("/login");
54+
}}
4155
>
42-
Add Todo
56+
SignOut
4357
</button>
44-
</Link>
58+
</div>
4559
</div>
4660
);
4761
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { fetchUserAttributes } from "aws-amplify/auth/server";
2+
import { data, Outlet, redirect } from "react-router";
3+
import type { Route } from "./+types/layout";
4+
import { runWithAmplifyServerContext } from "../../lib/amplifyServerUtils";
5+
6+
export async function loader({ request }: Route.LoaderArgs) {
7+
const responseHeaders = new Headers();
8+
return await runWithAmplifyServerContext({
9+
serverContext: { request, responseHeaders },
10+
operation: async (contextSpec) => {
11+
try {
12+
const user = await fetchUserAttributes(contextSpec);
13+
if (!user) {
14+
return redirect("/login", {
15+
// Redirect to /login
16+
headers: responseHeaders,
17+
});
18+
}
19+
return data(
20+
{}, // Return empty data
21+
{
22+
headers: responseHeaders,
23+
},
24+
);
25+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
26+
} catch (error: unknown) {
27+
return redirect("/login", {
28+
// Redirect to /login
29+
headers: responseHeaders,
30+
});
31+
}
32+
},
33+
});
34+
}
35+
36+
export default function Layout({ loaderData }: Route.ComponentProps) {
37+
return <Outlet />;
38+
}

examples/todo-app/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"typecheck": "react-router typegen && tsc"
1111
},
1212
"dependencies": {
13+
"@aws-amplify/ui-react": "^6.11.0",
1314
"@react-router/express": "^7.2.0",
1415
"@react-router/node": "^7.4.0",
1516
"@react-router/serve": "^7.4.0",

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
"lint-staged": "^15.4.3",
3434
"vitest": "^3.1.1"
3535
},
36+
"pnpm": {
37+
"overrides": {
38+
"@types/node": "^20.0.0"
39+
}
40+
},
3641
"lint-staged": {
3742
"*.ts": "pnpm lint"
3843
}

0 commit comments

Comments
 (0)