diff --git a/boilerplate/fullstack/remix/README.md b/boilerplate/fullstack/remix/README.md index a1622019..fc76dad0 100644 --- a/boilerplate/fullstack/remix/README.md +++ b/boilerplate/fullstack/remix/README.md @@ -1,10 +1,12 @@ -![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png) +# SuperTokens + Remix -# SuperTokens with Remix +A demo implementation of [SuperTokens](https://supertokens.com/) with [Remix](https://remix.run/). -This demo app demonstrates how to integrate SuperTokens into a Remix application. +## General Info -Feautures: +This project aims to demonstrate how to integrate SuperTokens into a Remix application. Its primary purpose is to serve as an educational tool, but it can also be used as a starting point for your own project. + +Features: - Initializes SuperTokens with frontend and backend configurations - Creates a frontend route to handle authentication-related tasks @@ -12,25 +14,29 @@ Feautures: - Protects frontend routes to ensure only authenticated users can access the dashboard - Exposes the SuperTokens authentication APIs used by frontend widgets -## Project structure & Parameters +## Repo Structure + +### Source ```txt -📦[your-app-name] +📦 ┣ 📂app ┃ ┣ 📂config -┃ ┃ ┣ 📜appInfo.tsx -┃ ┃ ┣ 📜backend.tsx -┃ ┃ ┗ 📜frontend.tsx +┃ ┃ ┣ 📜appInfo.tsx --> Includes information about your application reused throughout the app. +┃ ┃ ┣ 📜backend.tsx --> Backend-related configuration, including settings for SuperTokens. +┃ ┃ ┗ 📜frontend.tsx --> Frontend configuration, including settings for SuperTokens. ┃ ┣ 📂routes -┃ ┃ ┣ 📜_index.tsx -┃ ┃ ┣ 📜api.auth.$.tsx -┃ ┃ ┗ 📜auth.$.tsx +┃ ┃ ┣ 📜_index.tsx --> Public landing page - accessible regardless of auth state. +┃ ┃ ┣ 📜dashboard._index.tsx --> Protected route only accessible to authenticated users. +┃ ┃ ┣ 📜sessioninfo.$.tsx +┃ ┃ ┣ 📜supertokens.$.tsx +┃ ┃ ┗ 📜auth.$.tsx --> Deals with authentication routes or components using SuperTokens. ┃ ┣ 📜app.css -┃ ┣ 📜entry.server.tsx -┃ ┗ 📜root.tsx +┃ ┣ 📜entry.server.tsx --> Entry point for server-side rendering (SSR) setup. +┃ ┗ 📜root.tsx --> Root component of your application. ┣ 📂assets ┃ ┣ 📂fonts -┃ ┣ 📂images +┣ 📂images ┣ 📂test ┃ ┗ 📜basic.test.cjs ┣ 📜package.json @@ -38,65 +44,81 @@ Feautures: ┗ 📜server.mjs ``` -Let's explore the important files: +### Config -| Directory/File | Description | -| -------------------- | ----------------------------------------------------------------------------------------------- | -| **app** | Contains configuration files and route files for your application. | -| | **config** | -| | Contains configuration files for your application. | -| | `appInfo.tsx` : Includes information about your application reused throughout the app. | -| | `backend.tsx` : Backend-related configuration, including settings for SuperTokens. | -| | `frontend.tsx` : Frontend configuration, including settings for SuperTokens. | -| | **routes** | -| | Contains route files for your application. | -| | `_index.tsx` : Represents the default route or landing page. | -| | `api.auth.$.tsx` : Handles authentication-related API endpoints. | -| | `auth.$.tsx` : Deals with authentication routes or components using SuperTokens. | -| | `entry.server.tsx` : Entry point for server-side rendering (SSR) setup. | -| | `root.tsx` : Root component of your application. | -| **test** | Contains test files for your application. | -| | `basic.test.cjs` : A basic test file, presumably for testing functionality in your application. | -| **remix.config.mjs** | Remix configuration file containing settings for your Remix application. | -| **server.mjs** | File responsible for server-side functionality. | +#### Vite -## Run application locally +Remix uses Vite to compile your application. Everything available in the [Vite configuration docs](https://remix.run/docs/en/main/file-conventions/vite-config) is available to use here (refer to the `vite.config.ts` file). The only customization we've done is changing the port to `3000`. -Follow the steps outlined below to run the application locally: +#### SuperTokens -1. Change directory to the **[your-app-name]** folder. +The full configuration needed for SuperTokens (the frontend part) to work is in the `app/config` directory. This file will differ based on the [auth recipe](https://supertokens.com/docs/guides) you choose. - ```shell - cd your-app-name - ``` +If you choose to use this as a starting point for your own project, you can further customize the options and config in the `app/config` directory. Refer to our [docs](https://supertokens.com/docs) (and make sure to choose the correct recipe) for more details. -2. Run the application with the command below: +## Application Flow - ```shell - npm run dev - ``` +Remix is built on top of [React Router](https://reactrouter.com/). While you can configure routes via the ["routes" plugin option](https://remix.run/docs/en/main/file-conventions/vite-config#routes), most routes are created with the file system convention. Add a file, get a route. -## How to use +This Demo application consists of four main parts: -### Using `create-supertokens-app` +1. **Entry Point (`entry.server.tsx`)** -- Run the following command + - Initializes the Remix application on the server-side + - Handles SuperTokens initialization on the server-side -```bash -npx create-supertokens-app@latest --frontend=remix -``` +2. **Root Component (`root.tsx`)** + + - Initializes the Remix application + - Wraps the application with necessary providers: + - `SuperTokensWrapper`: Manages auth state and session + - renders the `App` component + - unprotected routes are rendered without the SessionAuth wrapper + - protected routes are rendered with the SessionAuth wrapper + +3. **Home page (`/` route, `/routes/_index.tsx` component)** + + - Public landing page accessible to all users + - Provides navigation to authentication and dashboard + - Displays basic application information and links + +4. **Auth (`/auth` route, `/routes/auth.&.tsx` component)** + + - Renders the SuperTokens' pre-built auth UI + +5. **Dashboard page (`/dashboard` route, `/routes/dashboard._index.tsx` component)** + - Protected route only accessible to authenticated users + - Protected by `SessionAuth` component + - Displays user information and session details + - Provides functionality to: + - View user ID + - Call test API endpoints + - Access documentation + - Sign out + +When a user visits the application, they start at the home page (`/`). They can choose to authenticate through the `/auth` route, and once authenticated, they gain access to the protected dashboard. The session state is managed throughout the application using SuperTokens' session management. + +## Customizations + +If you want to customize the default auth UI, you have two options: + +1. Refer to the [docs](https://supertokens.com/docs/thirdpartyemailpassword/advanced-customizations/react-component-override/usage) on how to customize the pre-built UI. +2. Roll your own UI by choosing "Custom UI" in the right sidebar in the [docs](https://supertokens.com/docs/thirdpartyemailpassword/quickstart/frontend-setup). + +## Additional resources -- Follow the instructions on screen +- Custom UI Example: https://github.com/supertokens/supertokens-web-js/tree/master/examples/react/with-thirdpartyemailpassword +- Custom UI Blog post: https://supertokens.medium.com/adding-social-login-to-your-website-with-supertokens-custom-ui-only-5fa4d7ab6402 +- Awesome SuperTokens: https://github.com/kohasummons/awesome-supertokens -## Author +## Contributing -Created with :heart: by the folks at supertokens.com. +Please refer to the [CONTRIBUTING.md](https://github.com/supertokens/create-supertokens-app/blob/master/CONTRIBUTING.md) file in the root of the [`create-supertokens-app`](https://github.com/supertokens/create-supertokens-app) repo. -## License +## Contact us -This project is licensed under the Apache 2.0 license. +For any questions, or support requests, please email us at team@supertokens.io, or join our [Discord](https://supertokens.io/discord) server. -## Notes +## Authors -- To know more about how this app works and to learn how to customise it based on your use cases refer to the [SuperTokens Documentation](https://supertokens.com/docs/guides) -- We have provided development OAuth keys for the various built-in third party providers in the `/app/config/backend.ts` file. Feel free to use them for development purposes, but **please create your own keys for production use**. +Created with :heart: by the folks at SuperTokens.io. diff --git a/boilerplate/fullstack/remix/app/app.css b/boilerplate/fullstack/remix/app/app.css index a1461c4e..d03d0e80 100644 --- a/boilerplate/fullstack/remix/app/app.css +++ b/boilerplate/fullstack/remix/app/app.css @@ -1,217 +1,179 @@ -html { - height: 100%; +/* Base styles */ +:root { + --primary-color: #ff9933; + --primary-hover: #ff8a15; + --success-color: #3eb655; + --success-bg: #e7ffed; + --border-color: #e0e0e0; } -.main { +@font-face { + font-family: Menlo; + src: url("../assets/fonts/MenloRegular.ttf"); +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", + "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; +} + +/* Layout */ +.app-container { display: flex; flex-direction: column; - align-items: center; - justify-content: center; + width: 100%; min-height: 100vh; + font-family: Rubik, sans-serif; } -.appContainer { - font-family: Rubik, sans-serif; +.fill { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; } -.appContainer * { - box-sizing: border-box; +/* Header */ +header { + border-bottom: 2px solid var(--border-color); + padding: 8px; } -.bold400 { - font-variation-settings: "wght" 400; +header nav { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; } -.bold500 { - font-variation-settings: "wght" 500; +header img { + width: 40px; } -.bold600 { - font-variation-settings: "wght" 600; +header ul { + display: flex; + gap: 12px; + list-style: none; + margin: 0; + padding: 0; } -.homeContainer { - min-height: 100vh; - min-width: 100vw; - background: url("../assets/images/background.png"); - background-size: cover; +header a { + color: var(--primary-color); + font-weight: 600; + text-decoration: none; +} +header a:hover { + color: var(--primary-hover); +} + +/* Home page */ +#home-container { + align-items: center; + min-height: calc(100vh - 58px); + background: url("../assets/images/background.png") center/cover; +} + +.logos { display: flex; - flex-direction: column; - justify-content: center; align-items: center; + gap: 20px; + padding-bottom: 80px; } -.bold700 { - font-variation-settings: "wght" 700; +.logos span { + font-size: 8rem; + font-weight: bold; +} + +.logos img { + height: 240px; + width: auto; } -.mainContainer { - box-shadow: 0px 0px 60px 0px rgba(0, 0, 0, 0.16); +/* Main content */ +.main-container { + box-shadow: 0 0 60px rgba(0, 0, 0, 0.16); width: min(635px, calc(100% - 24px)); border-radius: 16px; - margin-block-end: 159px; - background-color: #ffffff; + margin-bottom: 159px; + background-color: white; } -.successTitle { - line-height: 1; - padding-block: 26px; - background-color: #e7ffed; - text-align: center; - color: #3eb655; +.success-title { display: flex; justify-content: center; - align-items: flex-end; + align-items: center; + padding: 26px; + background-color: var(--success-bg); + color: var(--success-color); font-size: 20px; + line-height: 1; } -.successIcon { +.success-title img { margin-right: 8px; } -.innerContent { - padding-block: 48px; +.inner-content { + padding: 48px; text-align: center; display: flex; flex-direction: column; align-items: center; } -.userId { +/* User ID display */ +#user-id { position: relative; - padding: 14px 17px; - border-image-slice: 1; width: min(430px, calc(100% - 30px)); - margin-inline: auto; - margin-block: 11px 23px; + margin: 11px auto 23px; + padding: 14px 17px; border-radius: 9px; + font-family: Menlo, monospace; line-height: 1; - font-family: Menlo, serif; cursor: text; + border: 3px solid var(--primary-color); } -.userId:before { - content: ""; - position: absolute; - inset: 0; - border-radius: 9px; - padding: 2px; - background: linear-gradient(90.31deg, #ff9933 0.11%, #ff3f33 99.82%); - mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; -} - -.topBand, -.bottomBand { - border-radius: inherit; -} - -.topBand { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.bottomBand { - border-top-left-radius: 0; - border-top-right-radius: 0; +/* Buttons and navigation */ +.buttons { + display: flex; + gap: 4px; } -.sessionButton { - padding-left: 13px; - padding-right: 13px; - padding-top: 8px; - padding-bottom: 8px; - cursor: pointer; +.dashboard-button { + padding: 8px 13px; + background: var(--primary-color); + border: 1px solid var(--primary-hover); + border-radius: 6px; + box-shadow: 0 3px 6px rgba(255, 153, 51, 0.16); color: white; font-weight: bold; - font-size: 17px; - - box-sizing: border-box; - background: #ff9933; - border: 1px solid #ff8a15; - box-shadow: 0px 3px 6px rgba(255, 153, 51, 0.16); - border-radius: 6px; font-size: 16px; -} - -.bottomCTAContainer { - display: flex; - justify-content: flex-end; - padding-inline: 21px; - background-color: #212d4f; -} - -.viewCode { - padding-block: 11px; - color: #bac9f5; - cursor: pointer; - font-size: 14px; -} - -.bottomLinksContainer { - display: grid; - grid-template-columns: repeat(4, auto); - margin-bottom: 22px; -} - -.linksContainerLink { - display: flex; - align-items: center; - margin-inline-end: 68px; cursor: pointer; text-decoration: none; - color: #000000; -} - -.signOutLink { - border: 0; } -.linksContainerLink:last-child { - margin-right: 0; +/* Footer */ +footer { + padding: 10px; } -.truncate { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; +footer a { + color: var(--primary-color); + font-weight: 600; + /* text-decoration: none; */ } -.separatorLine { - max-width: 100%; -} - -.linkIcon { - width: 15px; - margin-right: 5px; -} - -@media screen and (max-width: 768px) { - .bottomLinksContainer { - grid-template-columns: repeat(2, 1fr); - column-gap: 64px; - row-gap: 34px; - } - - .linksContainerLink { - margin-inline-end: 0; - } - - .separatorLine { - max-width: 200px; - } -} - -@media screen and (max-width: 480px) { - .homeContainer { - justify-content: start; - padding-block-start: 25px; - } - - .mainContainer { - margin-block-end: 90px; - } +footer a:hover { + color: var(--primary-hover); } diff --git a/boilerplate/fullstack/remix/app/config/appInfo.tsx b/boilerplate/fullstack/remix/app/config/appInfo.tsx index 8f2a2479..80f09e99 100644 --- a/boilerplate/fullstack/remix/app/config/appInfo.tsx +++ b/boilerplate/fullstack/remix/app/config/appInfo.tsx @@ -1,6 +1,6 @@ export const appInfo = { // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo - appName: "SuperTokens Remix demo app", + appName: "SuperTokens + Remix", apiDomain: "http://localhost:3000", websiteDomain: "http://localhost:3000", apiBasePath: "/supertokens", diff --git a/boilerplate/fullstack/remix/app/config/frontend/all_auth.tsx b/boilerplate/fullstack/remix/app/config/frontend/all_auth.tsx index 8643f6ab..1636d07d 100644 --- a/boilerplate/fullstack/remix/app/config/frontend/all_auth.tsx +++ b/boilerplate/fullstack/remix/app/config/frontend/all_auth.tsx @@ -27,6 +27,12 @@ export const frontendConfig = (): SuperTokensConfig => { }), Session.init(), ], + getRedirectionURL: async (context: any) => { + if (context.action === "SUCCESS") { + return "/dashboard"; + } + return undefined; + }, }; }; diff --git a/boilerplate/fullstack/remix/app/config/frontend/emailpassword.tsx b/boilerplate/fullstack/remix/app/config/frontend/emailpassword.tsx index f1ef3185..587422d9 100644 --- a/boilerplate/fullstack/remix/app/config/frontend/emailpassword.tsx +++ b/boilerplate/fullstack/remix/app/config/frontend/emailpassword.tsx @@ -8,6 +8,12 @@ export const frontendConfig = (): SuperTokensConfig => { return { appInfo, recipeList: [EmailPasswordReact.init(), Session.init()], + getRedirectionURL: async (context: any) => { + if (context.action === "SUCCESS") { + return "/dashboard"; + } + return undefined; + }, }; }; diff --git a/boilerplate/fullstack/remix/app/config/frontend/multifactorauth.tsx b/boilerplate/fullstack/remix/app/config/frontend/multifactorauth.tsx index 51583bab..7da4d409 100644 --- a/boilerplate/fullstack/remix/app/config/frontend/multifactorauth.tsx +++ b/boilerplate/fullstack/remix/app/config/frontend/multifactorauth.tsx @@ -36,6 +36,12 @@ export const frontendConfig = (): SuperTokensConfig => { TOTPReact.init(), Session.init(), ], + getRedirectionURL: async (context: any) => { + if (context.action === "SUCCESS") { + return "/dashboard"; + } + return undefined; + }, }; }; diff --git a/boilerplate/fullstack/remix/app/config/frontend/passwordless.tsx b/boilerplate/fullstack/remix/app/config/frontend/passwordless.tsx index 9873152b..40fd9413 100644 --- a/boilerplate/fullstack/remix/app/config/frontend/passwordless.tsx +++ b/boilerplate/fullstack/remix/app/config/frontend/passwordless.tsx @@ -13,6 +13,12 @@ export const frontendConfig = (): SuperTokensConfig => { }), Session.init(), ], + getRedirectionURL: async (context: any) => { + if (context.action === "SUCCESS") { + return "/dashboard"; + } + return undefined; + }, }; }; diff --git a/boilerplate/fullstack/remix/app/config/frontend/thirdparty.tsx b/boilerplate/fullstack/remix/app/config/frontend/thirdparty.tsx index 2341e769..aa30fc4f 100644 --- a/boilerplate/fullstack/remix/app/config/frontend/thirdparty.tsx +++ b/boilerplate/fullstack/remix/app/config/frontend/thirdparty.tsx @@ -19,6 +19,12 @@ export const frontendConfig = (): SuperTokensConfig => { }), Session.init(), ], + getRedirectionURL: async (context: any) => { + if (context.action === "SUCCESS") { + return "/dashboard"; + } + return undefined; + }, }; }; diff --git a/boilerplate/fullstack/remix/app/config/frontend/thirdpartyemailpassword.tsx b/boilerplate/fullstack/remix/app/config/frontend/thirdpartyemailpassword.tsx index 791e9310..7a2c235a 100644 --- a/boilerplate/fullstack/remix/app/config/frontend/thirdpartyemailpassword.tsx +++ b/boilerplate/fullstack/remix/app/config/frontend/thirdpartyemailpassword.tsx @@ -22,6 +22,12 @@ export const frontendConfig = (): SuperTokensConfig => { }), Session.init(), ], + getRedirectionURL: async (context: any) => { + if (context.action === "SUCCESS") { + return "/dashboard"; + } + return undefined; + }, }; }; diff --git a/boilerplate/fullstack/remix/app/config/frontend/thirdpartypasswordless.tsx b/boilerplate/fullstack/remix/app/config/frontend/thirdpartypasswordless.tsx index 41e21b12..ea6e62bb 100644 --- a/boilerplate/fullstack/remix/app/config/frontend/thirdpartypasswordless.tsx +++ b/boilerplate/fullstack/remix/app/config/frontend/thirdpartypasswordless.tsx @@ -24,6 +24,12 @@ export const frontendConfig = (): SuperTokensConfig => { }), Session.init(), ], + getRedirectionURL: async (context: any) => { + if (context.action === "SUCCESS") { + return "/dashboard"; + } + return undefined; + }, }; }; diff --git a/boilerplate/fullstack/remix/app/root.tsx b/boilerplate/fullstack/remix/app/root.tsx index 44673a26..185abea6 100644 --- a/boilerplate/fullstack/remix/app/root.tsx +++ b/boilerplate/fullstack/remix/app/root.tsx @@ -1,4 +1,4 @@ -import { Meta, Links, Scripts, Outlet, ScrollRestoration, useLocation } from "@remix-run/react"; +import { Meta, Links, Scripts, Outlet, ScrollRestoration, useLocation, Link } from "@remix-run/react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { frontendConfig } from "./config/frontend"; import { SessionAuth } from "supertokens-auth-react/recipe/session/index.js"; @@ -12,7 +12,7 @@ if (typeof window !== "undefined") { export default function App() { const location = useLocation(); - const isUnprotectedRoute = location.pathname.startsWith("/auth"); + const isUnprotectedRoute = location.pathname.startsWith("/") || location.pathname.startsWith("/auth"); return ( @@ -20,18 +20,57 @@ export default function App() { - + - {isUnprotectedRoute ? ( - - ) : ( - - - - )} +
+
+ +
+
+ {isUnprotectedRoute ? ( + + ) : ( + + + + )} - - + + + + separator +
+
diff --git a/boilerplate/fullstack/remix/app/routes/_index.tsx b/boilerplate/fullstack/remix/app/routes/_index.tsx index e67c02e5..0b1a0b20 100644 --- a/boilerplate/fullstack/remix/app/routes/_index.tsx +++ b/boilerplate/fullstack/remix/app/routes/_index.tsx @@ -1,134 +1,46 @@ -import { CelebrateIcon, SeparatorLine, BlogsIcon, GuideIcon, SignOutIcon } from "../../assets/images"; -import { recipeDetails } from "../config/frontend"; -import SuperTokens from "supertokens-auth-react"; -import { LoaderFunctionArgs } from "@remix-run/node"; -import { useLoaderData, useNavigate } from "@remix-run/react"; -import SessionReact from "supertokens-auth-react/recipe/session/index.js"; -import { getSessionForSSR } from "supertokens-node/custom"; -import { TryRefreshComponent } from "../components/tryRefreshClientComponent"; -import { SessionAuthForRemix } from "../components/sessionAuthForRemix"; -import type { JWTPayload } from "jose"; - -export async function loader({ request }: LoaderFunctionArgs): Promise<{ - accessTokenPayload: JWTPayload | undefined; - hasToken: boolean; - error: Error | undefined; -}> { - return getSessionForSSR(request); -} +import { Link } from "@remix-run/react"; +import { useSessionContext } from "supertokens-auth-react/recipe/session"; export default function Home() { - const navigate = useNavigate(); - - const { accessTokenPayload, hasToken, error } = useLoaderData<{ - accessTokenPayload: JWTPayload | undefined; - hasToken: boolean; - error: Error | undefined; - }>(); - - async function logoutClicked() { - await SessionReact.signOut(); - SuperTokens.redirectToAuth(); - } + const session = useSessionContext(); - if (error) { - return
Something went wrong while trying to get the session. Error - {error.message}
; + if (session.loading) { + return null; } - - // `accessTokenPayload` will be undefined if it the session does not exist or has expired - if (accessTokenPayload === undefined) { - /** - * This means that the user is not logged in. If you want to display some other UI in this - * case, you can do so here. - */ - if (!hasToken) { - return navigate("/auth"); - } - /** - * This means that the session does not exist but we have session tokens for the user. In this case - * the `TryRefreshComponent` will try to refresh the session. - * - * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 - */ - return ; - } - - const fetchUserData = async () => { - const userInfoResponse = await fetch("http://localhost:3000/sessioninfo"); - - alert(JSON.stringify(await userInfoResponse.json())); - }; - - const links: { - name: string; - link: string; - icon: string; - }[] = [ - { - name: "Blogs", - link: "https://supertokens.com/blog", - icon: BlogsIcon, - }, - { - name: "Guides", - link: recipeDetails.docsLink, - icon: GuideIcon, - }, - { - name: "Sign Out", - link: "", - icon: SignOutIcon, - }, - ]; - - /** - * SessionAuthForRemix will handle proper redirection for the user based on the different session states. - * It will redirect to the login page if the session does not exist etc. - */ return ( - -
-
-
- Login successful - Login successful -
-
-
Your userID is:
- -
{accessTokenPayload.sub}
- - + <> +
+ SuperTokens + x + Remix +
+
+
+

+ SuperTokens x Remix
example project +

+
+ {session.doesSessionExist ? ( +

+ You're signed in already,
check out the Dashboard! 👇 +

+ ) : ( +

Sign-in to continue

+ )}
+
- -
- {links.map((link) => { - if (link.name === "Sign Out") { - return ( - - ); - } - return ( - - {link.name} -
{link.name}
-
- ); - })} -
- - separator -
- + + ); } diff --git a/boilerplate/fullstack/remix/app/routes/auth.$.tsx b/boilerplate/fullstack/remix/app/routes/auth.$.tsx index 9321471b..7ec098f3 100644 --- a/boilerplate/fullstack/remix/app/routes/auth.$.tsx +++ b/boilerplate/fullstack/remix/app/routes/auth.$.tsx @@ -15,7 +15,7 @@ export default function Auth() { }, []); if (loaded) { - return SuperTokensUI.getRoutingComponent(PreBuiltUIList); + return <>{SuperTokensUI.getRoutingComponent(PreBuiltUIList)}; } return null; diff --git a/boilerplate/fullstack/remix/app/routes/dashboard._index.tsx b/boilerplate/fullstack/remix/app/routes/dashboard._index.tsx new file mode 100644 index 00000000..2e84cadf --- /dev/null +++ b/boilerplate/fullstack/remix/app/routes/dashboard._index.tsx @@ -0,0 +1,94 @@ +import { useEffect } from "react"; +import type { JWTPayload } from "jose"; +import SuperTokens from "supertokens-auth-react"; +import { LoaderFunctionArgs } from "@remix-run/node"; +import { useLoaderData, useNavigate } from "@remix-run/react"; +import SessionReact from "supertokens-auth-react/recipe/session/index.js"; +import { getSessionForSSR } from "supertokens-node/custom"; +import { TryRefreshComponent } from "../components/tryRefreshClientComponent"; +import { SessionAuthForRemix } from "../components/sessionAuthForRemix"; + +export async function loader({ request }: LoaderFunctionArgs): Promise<{ + accessTokenPayload: JWTPayload | undefined; + hasToken: boolean; + error: Error | undefined; +}> { + return getSessionForSSR(request); +} + +export default function Dashboard() { + const navigate = useNavigate(); + + const { accessTokenPayload, hasToken, error } = useLoaderData<{ + accessTokenPayload: JWTPayload | undefined; + hasToken: boolean; + error: Error | undefined; + }>(); + + useEffect(() => { + if (!hasToken && accessTokenPayload === undefined) { + navigate("/auth"); + } + }, [hasToken, accessTokenPayload, navigate]); + + async function logoutClicked() { + await SessionReact.signOut(); + SuperTokens.redirectToAuth(); + } + + if (error) { + return
Something went wrong while trying to get the session. Error - {error.message}
; + } + + // `accessTokenPayload` will be undefined if it the session does not exist or has expired + if (accessTokenPayload === undefined) { + /** + * This means that the user is not logged in. If you want to display some other UI in this + * case, you can do so here. + */ + if (!hasToken) { + return null; + } + /** + * This means that the session does not exist but we have session tokens for the user. In this case + * the `TryRefreshComponent` will try to refresh the session. + * + * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 + */ + return ; + } + + const callAPIClicked = async () => { + const userInfoResponse = await fetch("http://localhost:3000/sessioninfo"); + alert(JSON.stringify(await userInfoResponse.json())); + }; + + /** + * SessionAuthForRemix will handle proper redirection for the user based on the different session states. + * It will redirect to the login page if the session does not exist etc. + */ + return ( + +
+
+ Login successful + Login successful +
+
+
Your userID is:
+
+ {accessTokenPayload.sub} +
+
+ + +
+
+
+
+ ); +} diff --git a/boilerplate/fullstack/remix/public/ST.svg b/boilerplate/fullstack/remix/public/ST.svg new file mode 100644 index 00000000..30b435bd --- /dev/null +++ b/boilerplate/fullstack/remix/public/ST.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/boilerplate/fullstack/remix/public/remix.svg b/boilerplate/fullstack/remix/public/remix.svg new file mode 100644 index 00000000..f8b1f6c3 --- /dev/null +++ b/boilerplate/fullstack/remix/public/remix.svg @@ -0,0 +1 @@ +Remix \ No newline at end of file