This repository has been archived by the owner on Jul 16, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from zmillman/remix-auth
Add authentication
- Loading branch information
Showing
15 changed files
with
378 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,3 +7,4 @@ cdk.out | |
|
||
# App build artifacts | ||
build | ||
.env.local |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { Box, Button, Flex } from "@radix-ui/themes"; | ||
import { Form, Link } from "@remix-run/react"; | ||
|
||
interface AppShellProps { | ||
user?: { | ||
email: string; | ||
name: string; | ||
}; | ||
} | ||
|
||
/** | ||
* Wraps content with a topbar for signing in / out | ||
*/ | ||
export default function AppShell( | ||
props: React.PropsWithChildren<AppShellProps>, | ||
) { | ||
return ( | ||
<Box> | ||
<Flex justify="between"> | ||
<Box> | ||
<Link to="/">Home</Link> | ||
</Box> | ||
<Box> | ||
{props.user ? ( | ||
<Form action="/logout" method="POST"> | ||
<Button type="submit">Sign out</Button> | ||
</Form> | ||
) : ( | ||
<Button asChild> | ||
<Link to="/login">Sign in</Link> | ||
</Button> | ||
)} | ||
</Box> | ||
</Flex> | ||
<Box>{props.children}</Box> | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Code, Heading, Text } from "@radix-ui/themes"; | ||
import { type LoaderFunctionArgs } from "@remix-run/node"; | ||
import { json, Link, useLoaderData } from "@remix-run/react"; | ||
import { getSession } from "../services/session.server"; | ||
|
||
export const loader = async ({ request }: LoaderFunctionArgs) => { | ||
const session = await getSession(request.headers.get("Cookie")); | ||
return json({ session: session.data }); | ||
}; | ||
|
||
export default function HomePage() { | ||
const data = useLoaderData<typeof loader>(); | ||
|
||
return ( | ||
<> | ||
<Heading mb="4">Welcome to the app</Heading> | ||
<Text mb="4" as="p"> | ||
This is an example demonstrating the Remix template. You can only view | ||
the <Link to="/dashboard">Dashboard</Link> if you're signed in. | ||
</Text> | ||
<Heading size="3">Session</Heading> | ||
<pre> | ||
<Code>{JSON.stringify(data.session, null, 2)}</Code> | ||
</pre> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { LoaderFunctionArgs } from "@remix-run/node"; | ||
import { authenticator } from "../services/auth.server"; | ||
|
||
export const loader = ({ request }: LoaderFunctionArgs) => { | ||
return authenticator.authenticate("google", request, { | ||
successRedirect: "/dashboard", | ||
failureRedirect: "/login", | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { ActionFunctionArgs, redirect } from "@remix-run/node"; | ||
import { authenticator } from "../services/auth.server"; | ||
|
||
export const loader = () => redirect("/login"); | ||
|
||
export const action = ({ request }: ActionFunctionArgs) => { | ||
return authenticator.authenticate("google", request); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Box, Heading, Text } from "@radix-ui/themes"; | ||
import { json, LoaderFunctionArgs } from "@remix-run/node"; | ||
import { useLoaderData } from "@remix-run/react"; | ||
import { authenticatedUser } from "../services/auth.server"; | ||
|
||
export const loader = async ({ request }: LoaderFunctionArgs) => { | ||
return json({ user: await authenticatedUser(request) }); | ||
}; | ||
|
||
export default function DashboardPage() { | ||
const data = useLoaderData<typeof loader>(); | ||
|
||
return ( | ||
<Box> | ||
<Heading mb="4">Dashboard</Heading> | ||
<Text as="p" mb="4"> | ||
Welcome to the dashboard, {data.user.name}! | ||
</Text> | ||
<Text as="p" mb="4"> | ||
You're only able to see this page if you're signed in. | ||
</Text> | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { Box, Button, Callout, Card, Heading } from "@radix-ui/themes"; | ||
import { LoaderFunctionArgs } from "@remix-run/node"; | ||
import { Form, json, redirect, useLoaderData } from "@remix-run/react"; | ||
import { authenticator } from "../services/auth.server"; | ||
import { InfoCircledIcon } from "@radix-ui/react-icons"; | ||
import { commitSession, getSession } from "../services/session.server"; | ||
|
||
export async function loader({ request }: LoaderFunctionArgs) { | ||
// If the user is already authenticated redirect to /dashboard directly | ||
const user = await authenticator.isAuthenticated(request, {}); | ||
|
||
if (user) { | ||
return redirect("/dashboard"); | ||
} else { | ||
const session = await getSession(request.headers.get("Cookie")); | ||
const authError = session.get(authenticator.sessionErrorKey); | ||
|
||
return json( | ||
{ authErrorMessage: authError?.message }, | ||
{ | ||
headers: { | ||
"Set-Cookie": await commitSession(session), // clear flash message from the cookie | ||
}, | ||
}, | ||
); | ||
} | ||
} | ||
|
||
export default function LoginPage() { | ||
const data = useLoaderData<typeof loader>(); | ||
|
||
return ( | ||
<Card> | ||
<Box> | ||
<Heading mb="4">Sign in</Heading> | ||
{data.authErrorMessage ? ( | ||
<Callout.Root color="red" mb="2"> | ||
<Callout.Icon> | ||
<InfoCircledIcon /> | ||
</Callout.Icon> | ||
<Callout.Text>{data.authErrorMessage}</Callout.Text> | ||
</Callout.Root> | ||
) : ( | ||
<></> | ||
)} | ||
<Form action="/auth/google" method="POST"> | ||
<Button type="submit">Sign in with Google</Button> | ||
</Form> | ||
</Box> | ||
</Card> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { ActionFunctionArgs } from "@remix-run/node"; | ||
import { authenticator } from "../services/auth.server"; | ||
|
||
export async function action({ request }: ActionFunctionArgs) { | ||
await authenticator.logout(request, { redirectTo: "/login" }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Authenticator } from "remix-auth"; | ||
import { commitSession, getSession, sessionStorage } from "./session.server"; | ||
import { GoogleStrategy } from "remix-auth-google"; | ||
import { redirect } from "@remix-run/server-runtime"; | ||
|
||
// TODO: Make this a persisted schema | ||
interface User { | ||
name: string; | ||
email: string; | ||
} | ||
|
||
// Create an instance of the authenticator, pass a generic with what | ||
// strategies will return and will store in the session | ||
export const authenticator = new Authenticator<User>(sessionStorage, { | ||
sessionErrorKey: "auth-error", | ||
throwOnError: true, | ||
}); | ||
|
||
const googleClientId = process.env.GOOGLE_CLIENT_ID; | ||
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET; | ||
|
||
if (!googleClientId) { | ||
throw new Error("You must provide a GOOGLE_CLIENT_ID"); | ||
} | ||
if (!googleClientSecret) { | ||
throw new Error("You must provide a GOOGLE_CLIENT_SECRET"); | ||
} | ||
|
||
const googleStrategy = new GoogleStrategy( | ||
{ | ||
clientID: googleClientId, | ||
clientSecret: googleClientSecret, | ||
callbackURL: "http://localhost:3000/auth/google/callback", | ||
prompt: "select_account", // always ask the user to pick their account | ||
}, | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
async ({ accessToken, refreshToken, extraParams, profile }) => { | ||
// Get the user data from your DB or API using the tokens and profile | ||
// return User.findOrCreate({ email: profile.emails[0].value }) | ||
|
||
return { name: profile.displayName, email: profile.emails[0].value }; | ||
}, | ||
); | ||
|
||
authenticator.use(googleStrategy); | ||
|
||
/** | ||
* Get the authenticated user, or redirect to `/login` if they're not signed in | ||
*/ | ||
export const authenticatedUser = async (request: Request) => { | ||
const user = await authenticator.isAuthenticated(request); | ||
|
||
if (user) { | ||
// TODO: load user data from the database instead of storing in the session | ||
return user; | ||
} else { | ||
const session = await getSession(request.headers.get("Cookie")); | ||
session.flash(authenticator.sessionErrorKey, { | ||
message: "You must be signed in to view that page", | ||
}); | ||
throw redirect("/login", { | ||
headers: { "Set-Cookie": await commitSession(session) }, // persist flash message | ||
}); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Copied from https://github.com/sergiodxa/remix-auth/blob/main/README.md | ||
|
||
import { createCookieSessionStorage } from "@remix-run/node"; | ||
|
||
const authSecret = process.env.AUTH_SECRET; | ||
if (!authSecret) { | ||
throw new Error("You must provide a AUTH_SECRET"); | ||
} | ||
|
||
// export the whole sessionStorage object | ||
export const sessionStorage = createCookieSessionStorage({ | ||
cookie: { | ||
name: "_session", // use any name you want here | ||
sameSite: "lax", // this helps with CSRF | ||
path: "/", // remember to add this so the cookie will work in all routes | ||
httpOnly: true, // for security reasons, make this cookie http only | ||
secrets: [authSecret], // token to encrypt content | ||
secure: process.env.NODE_ENV === "production", // enable this in prod only | ||
}, | ||
}); | ||
|
||
// you can also export the methods individually for your own usage | ||
export const { getSession, commitSession, destroySession } = sessionStorage; |
Oops, something went wrong.