-
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.
- Loading branch information
Showing
19 changed files
with
1,081 additions
and
14 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
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,26 @@ | ||
'use client'; | ||
|
||
import { Container, Spinner } from '@chakra-ui/react'; | ||
import React, { Suspense } from 'react'; | ||
|
||
import { DefaultFooter } from '../../components/organisms/DefaultFooter'; | ||
import { DefaultHeader } from '../../components/organisms/DefaultHeader'; | ||
import type { LayoutComponent } from '../../types'; | ||
|
||
const DefaultLayout: LayoutComponent = ({ children }) => { | ||
return ( | ||
<> | ||
<DefaultHeader /> | ||
|
||
<Suspense fallback={<Spinner left="50%" position="fixed" top="50%" transform="translate(-50%, -50%)" />}> | ||
<Container pb={16} pt={8}> | ||
{children} | ||
</Container> | ||
</Suspense> | ||
|
||
<DefaultFooter /> | ||
</> | ||
); | ||
}; | ||
|
||
export default DefaultLayout; |
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 type { NextRequest } from 'next/server'; | ||
import { NextResponse } from 'next/server'; | ||
import { getAppDirRequestHandler } from 'supertokens-node/nextjs'; | ||
|
||
import { ensureSuperTokensInit } from '../../../../infrastructures/supertokens/backendConfig'; | ||
|
||
ensureSuperTokensInit(); | ||
|
||
const handleCall = getAppDirRequestHandler(NextResponse); | ||
|
||
export async function GET(request: NextRequest): Promise<Response> { | ||
const res = await handleCall(request); | ||
if (!res.headers.has('Cache-Control')) { | ||
// This is needed for production deployments with Vercel | ||
res.headers.set('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate'); | ||
} | ||
return res; | ||
} | ||
|
||
export async function POST(request: NextRequest): Promise<Response> { | ||
return handleCall(request); | ||
} | ||
|
||
export async function DELETE(request: NextRequest): Promise<Response> { | ||
return handleCall(request); | ||
} | ||
|
||
export async function PUT(request: NextRequest): Promise<Response> { | ||
return handleCall(request); | ||
} | ||
|
||
export async function PATCH(request: NextRequest): Promise<Response> { | ||
return handleCall(request); | ||
} | ||
|
||
export async function HEAD(request: NextRequest): Promise<Response> { | ||
return handleCall(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,26 @@ | ||
'use client'; | ||
|
||
import type { NextPage } from 'next'; | ||
import { useEffect, useState } from 'react'; | ||
import { redirectToAuth } from 'supertokens-auth-react'; | ||
import { EmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/emailpassword/prebuiltui'; | ||
import SuperTokens from 'supertokens-auth-react/ui'; | ||
|
||
const AuthPage: NextPage = () => { | ||
// if the user visits a page that is not handled by us (like /auth/random), then we redirect them back to the auth page. | ||
const [loaded, setLoaded] = useState(false); | ||
|
||
useEffect(() => { | ||
if (SuperTokens.canHandleRoute([EmailPasswordPreBuiltUI])) { | ||
setLoaded(true); | ||
} else { | ||
void redirectToAuth({ redirectBack: false }); | ||
} | ||
}, []); | ||
|
||
if (loaded) { | ||
return SuperTokens.getRoutingComponent([EmailPasswordPreBuiltUI]); | ||
} | ||
}; | ||
|
||
export default AuthPage; |
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,14 @@ | ||
import type { NextPage } from 'next'; | ||
|
||
import { HomePage } from '../../components/pages/homePage'; | ||
|
||
const CoursePage: NextPage = async () => { | ||
return ( | ||
<main> | ||
<h1>Courses</h1> | ||
<HomePage /> | ||
</main> | ||
); | ||
}; | ||
|
||
export default CoursePage; |
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,139 @@ | ||
import { serialize } from 'cookie'; | ||
import { cookies, headers } from 'next/headers'; | ||
import type { NextRequest } from 'next/server'; | ||
import { NextResponse } from 'next/server'; | ||
import { PreParsedRequest, CollectingResponse } from 'supertokens-node/framework/custom'; | ||
import type { SessionContainer, VerifySessionOptions } from 'supertokens-node/recipe/session'; | ||
import Session from 'supertokens-node/recipe/session'; | ||
import type { HTTPMethod } from 'supertokens-node/types'; | ||
|
||
import { ensureSuperTokensInit } from '../infrastructures/supertokens/backendConfig'; | ||
|
||
ensureSuperTokensInit(); | ||
|
||
interface SSRSession { | ||
session: SessionContainer | undefined; | ||
hasToken: boolean; | ||
/** | ||
* This allows us to protect our routes based on the current session claims. For example | ||
* this will be true if email verification is required but the user has not verified their | ||
* email. | ||
*/ | ||
hasInvalidClaims: boolean; | ||
baseResponse: CollectingResponse; | ||
nextResponse?: NextResponse; | ||
} | ||
|
||
export async function getSSRSession(req?: NextRequest, options?: VerifySessionOptions): Promise<SSRSession> { | ||
const query = req === undefined ? {} : Object.fromEntries(new URL(req.url).searchParams.entries()); | ||
const parsedCookies: Record<string, string> = Object.fromEntries( | ||
(req === undefined ? cookies() : req.cookies).getAll().map((cookie) => [cookie.name, cookie.value]) | ||
); | ||
|
||
/** | ||
* Pre parsed request is a wrapper exposed by SuperTokens. It is used as a helper to detect if the | ||
* original request contains session tokens. We then use this pre parsed request to call `getSession` | ||
* to check if there is a valid session. | ||
*/ | ||
const baseRequest = new PreParsedRequest({ | ||
method: req === undefined ? 'get' : (req.method as HTTPMethod), | ||
url: req === undefined ? '' : req.url, | ||
query, | ||
headers: req === undefined ? headers() : req.headers, | ||
cookies: parsedCookies, | ||
getFormBody: () => req!.formData(), | ||
getJSONBody: () => req!.json(), | ||
}); | ||
|
||
/** | ||
* Collecting response is a wrapper exposed by SuperTokens. In this case we are using an empty | ||
* CollectingResponse when calling `getSession`. If the request contains valid session tokens | ||
* the SuperTokens SDK will attach all the relevant tokens to the collecting response object which | ||
* we can then use to return those session tokens in the final result (refer to `withSession` in this file) | ||
*/ | ||
const baseResponse = new CollectingResponse(); | ||
|
||
try { | ||
/** | ||
* `getSession` will throw if session is required and there is no valid session. You can use | ||
* `options` to configure whether or not you want to require sessions when calling `getSSRSession` | ||
*/ | ||
const session = await Session.getSession(baseRequest, baseResponse, options); | ||
return { | ||
session, | ||
hasInvalidClaims: false, | ||
hasToken: session !== undefined, | ||
baseResponse, | ||
}; | ||
} catch (error) { | ||
if (Session.Error.isErrorFromSuperTokens(error)) { | ||
return { | ||
hasToken: error.type !== Session.Error.UNAUTHORISED, | ||
hasInvalidClaims: error.type === Session.Error.INVALID_CLAIMS, | ||
session: undefined, | ||
baseResponse, | ||
nextResponse: new NextResponse('Authentication required', { | ||
status: error.type === Session.Error.INVALID_CLAIMS ? 403 : 401, | ||
}), | ||
}; | ||
} else { | ||
throw error; | ||
} | ||
} | ||
} | ||
|
||
export async function withSession( | ||
request: NextRequest, | ||
handler: (session: SessionContainer | undefined) => Promise<NextResponse>, | ||
options?: VerifySessionOptions | ||
): Promise<NextResponse<unknown>> { | ||
const { baseResponse, nextResponse, session } = await getSSRSession(request, options); | ||
if (nextResponse) { | ||
return nextResponse; | ||
} | ||
|
||
const userResponse = await handler(session); | ||
|
||
let didAddCookies = false; | ||
let didAddHeaders = false; | ||
|
||
/** | ||
* Base response is the response from SuperTokens that contains all the session tokens. | ||
* We add all cookies and headers in the base response to the final response from the | ||
* API to make sure sessions work correctly. | ||
*/ | ||
for (const respCookie of baseResponse.cookies) { | ||
didAddCookies = true; | ||
userResponse.headers.append( | ||
'Set-Cookie', | ||
serialize(respCookie.key, respCookie.value, { | ||
domain: respCookie.domain, | ||
expires: new Date(respCookie.expires), | ||
httpOnly: respCookie.httpOnly, | ||
path: respCookie.path, | ||
sameSite: respCookie.sameSite, | ||
secure: respCookie.secure, | ||
}) | ||
); | ||
} | ||
|
||
for (const [value, key] of baseResponse.headers) { | ||
didAddHeaders = true; | ||
userResponse.headers.set(key, value); | ||
} | ||
|
||
/** | ||
* For some deployment services (Vercel for example) production builds can return cached results for | ||
* APIs with older header values. In this case if the session tokens have changed (because of refreshing | ||
* for example) the cached result would still contain the older tokens and sessions would stop working. | ||
* | ||
* As a result, if we add cookies or headers from base response we also set the Cache-Control header | ||
* to make sure that the final result is not a cached version. | ||
*/ | ||
if ((didAddCookies || didAddHeaders) && !userResponse.headers.has('Cache-Control')) { | ||
// This is needed for production deployments with Vercel | ||
userResponse.headers.set('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate'); | ||
} | ||
|
||
return userResponse; | ||
} |
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 @@ | ||
'use client'; | ||
|
||
import React from 'react'; | ||
import { useSessionContext } from 'supertokens-auth-react/recipe/session'; | ||
|
||
export const HomeClientComponent: React.FC = () => { | ||
const session = useSessionContext(); | ||
|
||
if (session.loading) { | ||
return <div>Loading...</div>; | ||
} | ||
|
||
if (session.doesSessionExist === false) { | ||
return <div>Session does not exist</div>; | ||
} | ||
|
||
return ( | ||
<div> | ||
<div> | ||
<p> | ||
Client side component got userId: {session.userId} | ||
<br /> | ||
</p> | ||
</div> | ||
</div> | ||
); | ||
}; |
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,19 @@ | ||
'use client'; | ||
|
||
import React, { useEffect, useState } from 'react'; | ||
import { SessionAuth } from 'supertokens-auth-react/recipe/session'; | ||
|
||
type Props = Parameters<typeof SessionAuth>[0] & { | ||
children?: React.ReactNode | undefined; | ||
}; | ||
|
||
export const SessionAuthForNext: React.FC<Props> = (props: Props) => { | ||
const [loaded, setLoaded] = useState(false); | ||
useEffect(() => { | ||
setLoaded(true); | ||
}, []); | ||
if (!loaded) { | ||
return props.children; | ||
} | ||
return <SessionAuth {...props}>{props.children}</SessionAuth>; | ||
}; |
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,43 @@ | ||
'use client'; | ||
|
||
import { useRouter } from 'next/navigation'; | ||
import React, { useEffect, useState } from 'react'; | ||
import SuperTokens from 'supertokens-auth-react'; | ||
import Session from 'supertokens-auth-react/recipe/session'; | ||
|
||
export const TryRefreshComponent: React.FC = () => { | ||
const router = useRouter(); | ||
const [didError, setDidError] = useState(false); | ||
|
||
useEffect(() => { | ||
/** | ||
* `attemptRefreshingSession` will call the refresh token endpoint to try and | ||
* refresh the session. This will throw an error if the session cannot be refreshed. | ||
*/ | ||
void Session.attemptRefreshingSession() | ||
.then((hasSession) => { | ||
/** | ||
* If the user has a valid session, we reload the page to restart the flow | ||
* with valid session tokens | ||
*/ | ||
if (hasSession) { | ||
router.refresh(); | ||
} else { | ||
SuperTokens.redirectToAuth(); | ||
} | ||
}) | ||
.catch(() => { | ||
setDidError(true); | ||
}); | ||
}, [router]); | ||
|
||
/** | ||
* We add this check to make sure we handle the case where the refresh API fails with | ||
* an unexpected error | ||
*/ | ||
if (didError) { | ||
return <div>Something went wrong, please reload the page</div>; | ||
} | ||
|
||
return <div>Loading...</div>; | ||
}; |
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,14 @@ | ||
import type { BoxProps } from '@chakra-ui/react'; | ||
import { Box, HStack } from '@chakra-ui/react'; | ||
|
||
import { APP_AUTHOR } from '../../constants'; | ||
|
||
export const DefaultFooter: React.FC<BoxProps> = (props) => { | ||
return ( | ||
<Box px={4} {...props}> | ||
<HStack borderTopWidth={1} color="gray.500" fontSize="sm" py={4} spacing={8}> | ||
<div>© {APP_AUTHOR}</div> | ||
</HStack> | ||
</Box> | ||
); | ||
}; |
Oops, something went wrong.