From 8339c9854383b69eaed87376d8fd2b2d977e1950 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 4 Nov 2020 20:10:33 -0300 Subject: [PATCH 1/5] Add withAuth HOC --- src/handlers/login.ts | 2 +- src/hooks/index.ts | 2 ++ src/hooks/use-user.tsx | 12 ++++--- src/hooks/with-auth.tsx | 67 ++++++++++++++++++++++++++++++++++++++++ src/index.browser.ts | 2 +- src/index.ts | 2 +- src/utils/url-helpers.ts | 14 ++++++++- 7 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/with-auth.tsx diff --git a/src/handlers/login.ts b/src/handlers/login.ts index 928ecee23..bf69fc78e 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -1,6 +1,6 @@ import { NextApiResponse, NextApiRequest } from 'next'; import { ClientFactory, Config, loginHandler as getLoginHandler, LoginOptions } from '../auth0-session'; -import isSafeRedirect from '../utils/url-helpers'; +import { isSafeRedirect } from '../utils/url-helpers'; import TransientCookieHandler from '../auth0-session/transient-handler'; export default function loginHandler( diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 000000000..f89de3621 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,2 @@ +export { default as UserProvider, UserProfile, UserContext, useUser } from './use-user'; +export { default as withAuth } from './with-auth'; diff --git a/src/hooks/use-user.tsx b/src/hooks/use-user.tsx index d932fb7f6..3de40817f 100644 --- a/src/hooks/use-user.tsx +++ b/src/hooks/use-user.tsx @@ -11,23 +11,25 @@ export interface UserProfile { [key: string]: unknown; // Any custom claim which could be in the profile } +export type NullableUserProfile = UserProfile | null; + export interface UserContext { - user: UserProfile | null; + user: NullableUserProfile; loading: boolean; } -type UserProviderProps = React.PropsWithChildren<{ user: UserProfile | null }>; - const User = createContext({ user: null, loading: false }); export const useUser = (): UserContext => useContext(User); +type UserProviderProps = React.PropsWithChildren<{ user: NullableUserProfile }>; + export default ({ children, user: initialUser }: UserProviderProps): ReactElement => { - const [user, setUser] = useState(() => initialUser); // if used withAuth, initialUser is populated + const [user, setUser] = useState(() => initialUser); // with withAuth, initialUser gets populated const [loading, setLoading] = useState(() => !initialUser); // if initialUser is populated, no loading needed useEffect((): void => { - if (user) return; // if initialUser is populated, no loading required + if (user) return; // if initialUser is populated, no loading needed (async (): Promise => { const response = await fetch('/api/me'); diff --git a/src/hooks/with-auth.tsx b/src/hooks/with-auth.tsx new file mode 100644 index 000000000..078dea2e3 --- /dev/null +++ b/src/hooks/with-auth.tsx @@ -0,0 +1,67 @@ +import React, { Component } from 'react'; +import { NextApiRequest, NextApiResponse, NextPage, NextPageContext } from 'next'; +import Router from 'next/router'; + +import { NullableUserProfile, useUser } from './use-user'; +import { ISignInWithAuth0 } from '../instance'; +import { createLoginUrl } from '../utils/url-helpers'; + +type RedirectToLoginProps = { + render: () => React.ReactElement; +}; + +class RedirectToLogin extends Component { + public constructor(props: RedirectToLoginProps) { + super(props); + } + + public componentDidMount(): void { + window.location.assign(createLoginUrl(Router.pathname)); + } + + public render(): React.ReactElement { + return this.props.render(); + } +} + +type AuthenticatedProps = React.PropsWithChildren<{ user: NullableUserProfile }>; + +export default function withAuth( + InnerComponent: React.ElementType, + redirect: () => React.ReactElement, + instance: ISignInWithAuth0 +): NextPage { + const Authenticated: NextPage = (props) => { + const { user } = useUser(); + + if (!user) { + return ; + } + + return ; + }; + + Authenticated.getInitialProps = async (context: NextPageContext): Promise => { + if (!context.req) { + const response = await fetch('/api/me'); + const result = response.ok ? await response.json() : null; + + return { user: result, children: undefined }; + } + + const session = await instance.getSession(context.req as NextApiRequest, context.res as NextApiResponse); + + if (!session || !session.user) { + context.res?.writeHead(302, { + Location: createLoginUrl(context.req.url) + }); + context.res?.end(); + + return { user: null, children: undefined }; + } + + return { user: session.user as NullableUserProfile, children: undefined }; + }; + + return Authenticated; +} diff --git a/src/index.browser.ts b/src/index.browser.ts index 13e3d716d..2f6d3a3c6 100644 --- a/src/index.browser.ts +++ b/src/index.browser.ts @@ -3,7 +3,7 @@ import { ConfigParameters } from './auth0-session'; import Instance from './instance.browser'; import { ISignInWithAuth0 } from './instance'; -export { default as UserProvider, UserProfile, UserContext, useUser } from './hooks/use-user'; +export { UserProvider, UserProfile, UserContext, useUser, withAuth } from './hooks'; // @ts-ignore un-used settings export function initAuth0(config: ConfigParameters): ISignInWithAuth0 { diff --git a/src/index.ts b/src/index.ts index 670439466..8969ceed9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { ConfigParameters } from './auth0-session'; import { ISignInWithAuth0 } from './instance'; -export { default as UserProvider, UserProfile, UserContext, useUser } from './hooks/use-user'; +export { UserProvider, UserProfile, UserContext, useUser, withAuth } from './hooks'; export function initAuth0(settings: ConfigParameters): ISignInWithAuth0 { const isBrowser = typeof window !== 'undefined' || (process as any).browser; diff --git a/src/utils/url-helpers.ts b/src/utils/url-helpers.ts index ac0fbcf65..3cf497daf 100644 --- a/src/utils/url-helpers.ts +++ b/src/utils/url-helpers.ts @@ -2,7 +2,7 @@ * Helper which tests if a URL can safely be redirected to. Requires the URL to be relative. * @param url */ -export default function isSafeRedirect(url: string): boolean { +export function isSafeRedirect(url: string): boolean { if (typeof url !== 'string') { throw new TypeError(`Invalid url: ${url}`); } @@ -14,3 +14,15 @@ export default function isSafeRedirect(url: string): boolean { return !/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url); } + +/** + * Helper which creates the login URL to redirect to. + * @param redirectTo + */ +export function createLoginUrl(redirectTo?: string): string { + if (redirectTo) { + return `/api/login?redirectTo=${encodeURIComponent(redirectTo)}`; + } + + return `/api/login`; +} From f9c787ee2b464e6427a68d8ff2b998e8e88cb915 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 4 Nov 2020 20:16:19 -0300 Subject: [PATCH 2/5] Update basic sample --- .../components/login-redirect.jsx | 24 ++++------- .../basic-example/components/with-auth.jsx | 42 ------------------- examples/basic-example/pages/profile-ssr.jsx | 6 ++- .../basic-example/pages/protected-page.jsx | 7 ++-- 4 files changed, 16 insertions(+), 63 deletions(-) delete mode 100644 examples/basic-example/components/with-auth.jsx diff --git a/examples/basic-example/components/login-redirect.jsx b/examples/basic-example/components/login-redirect.jsx index 0f6e1baee..284265231 100644 --- a/examples/basic-example/components/login-redirect.jsx +++ b/examples/basic-example/components/login-redirect.jsx @@ -1,19 +1,11 @@ -import Router from 'next/router'; -import React, { Component } from 'react'; +import React from 'react'; -import Layout from '../components/layout'; -import createLoginUrl from '../lib/url-helper'; +import Layout from './layout'; -export default class RedirectToLogin extends Component { - componentDidMount() { - window.location.assign(createLoginUrl(Router.pathname)); - } +const LoginRedirect = () => ( + +
Signing you in...
+
+); - render() { - return ( - -
Signing you in...
-
- ); - } -} +export default LoginRedirect; diff --git a/examples/basic-example/components/with-auth.jsx b/examples/basic-example/components/with-auth.jsx deleted file mode 100644 index 6d82afc17..000000000 --- a/examples/basic-example/components/with-auth.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { useUser } from '@auth0/nextjs-auth0'; - -import auth0 from '../lib/auth0'; -import createLoginUrl from '../lib/url-helper'; -import RedirectToLogin from '../components/login-redirect'; - -export default function withAuth(InnerComponent) { - const Authenticated = (props) => { - const { user } = useUser(); - - if (!user) { - return ; // do you need a "redirecting to login" route? - } - - return ; - }; - - Authenticated.getInitialProps = async (ctx) => { - if (!ctx.req) { - const response = await fetch('/api/me'); - const result = response.ok ? await response.json() : null; - - return { user: result }; - } - - const session = await auth0.getSession(ctx.req, ctx.res); - - if (!session || !session.user) { - ctx.res.writeHead(302, { - Location: createLoginUrl(ctx.req.url) - }); - ctx.res.end(); - - return; - } - - return { user: session.user }; - }; - - return Authenticated; -} diff --git a/examples/basic-example/pages/profile-ssr.jsx b/examples/basic-example/pages/profile-ssr.jsx index 66246b2f8..365160404 100644 --- a/examples/basic-example/pages/profile-ssr.jsx +++ b/examples/basic-example/pages/profile-ssr.jsx @@ -1,7 +1,9 @@ import React from 'react'; +import { withAuth } from '@auth0/nextjs-auth0'; import Layout from '../components/layout'; -import withAuth from '../components/with-auth'; +import LoginRedirect from '../components/login-redirect'; +import auth0 from '../lib/auth0'; const Profile = ({ user }) => ( @@ -14,4 +16,4 @@ const Profile = ({ user }) => ( ); -export default withAuth(Profile); +export default withAuth(Profile, LoginRedirect, auth0); diff --git a/examples/basic-example/pages/protected-page.jsx b/examples/basic-example/pages/protected-page.jsx index 9df18ab85..a11b76959 100644 --- a/examples/basic-example/pages/protected-page.jsx +++ b/examples/basic-example/pages/protected-page.jsx @@ -1,8 +1,9 @@ import React from 'react'; -import { useUser } from '@auth0/nextjs-auth0'; +import { useUser, withAuth } from '@auth0/nextjs-auth0'; import Layout from '../components/layout'; -import withAuth from '../components/with-auth'; +import LoginRedirect from '../components/login-redirect'; +import auth0 from '../lib/auth0'; export function ProtectedPage() { const { user, loading } = useUser(); @@ -23,4 +24,4 @@ export function ProtectedPage() { ); } -export default withAuth(ProtectedPage); +export default withAuth(ProtectedPage, LoginRedirect, auth0); From 736d78a9d4f14c257da5c9bfe3bd20018a770a6b Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 4 Nov 2020 20:24:31 -0300 Subject: [PATCH 3/5] Update api call example --- .../components/login-redirect.jsx | 24 ++++------- .../api-call-example/components/with-auth.jsx | 42 ------------------- examples/api-call-example/lib/url-helper.js | 6 --- .../api-call-example/pages/profile-ssr.jsx | 6 ++- .../api-call-example/pages/protected-page.jsx | 7 ++-- examples/basic-example/lib/url-helper.js | 6 --- 6 files changed, 16 insertions(+), 75 deletions(-) delete mode 100644 examples/api-call-example/components/with-auth.jsx delete mode 100644 examples/api-call-example/lib/url-helper.js delete mode 100644 examples/basic-example/lib/url-helper.js diff --git a/examples/api-call-example/components/login-redirect.jsx b/examples/api-call-example/components/login-redirect.jsx index 0f6e1baee..284265231 100644 --- a/examples/api-call-example/components/login-redirect.jsx +++ b/examples/api-call-example/components/login-redirect.jsx @@ -1,19 +1,11 @@ -import Router from 'next/router'; -import React, { Component } from 'react'; +import React from 'react'; -import Layout from '../components/layout'; -import createLoginUrl from '../lib/url-helper'; +import Layout from './layout'; -export default class RedirectToLogin extends Component { - componentDidMount() { - window.location.assign(createLoginUrl(Router.pathname)); - } +const LoginRedirect = () => ( + +
Signing you in...
+
+); - render() { - return ( - -
Signing you in...
-
- ); - } -} +export default LoginRedirect; diff --git a/examples/api-call-example/components/with-auth.jsx b/examples/api-call-example/components/with-auth.jsx deleted file mode 100644 index 6d82afc17..000000000 --- a/examples/api-call-example/components/with-auth.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { useUser } from '@auth0/nextjs-auth0'; - -import auth0 from '../lib/auth0'; -import createLoginUrl from '../lib/url-helper'; -import RedirectToLogin from '../components/login-redirect'; - -export default function withAuth(InnerComponent) { - const Authenticated = (props) => { - const { user } = useUser(); - - if (!user) { - return ; // do you need a "redirecting to login" route? - } - - return ; - }; - - Authenticated.getInitialProps = async (ctx) => { - if (!ctx.req) { - const response = await fetch('/api/me'); - const result = response.ok ? await response.json() : null; - - return { user: result }; - } - - const session = await auth0.getSession(ctx.req, ctx.res); - - if (!session || !session.user) { - ctx.res.writeHead(302, { - Location: createLoginUrl(ctx.req.url) - }); - ctx.res.end(); - - return; - } - - return { user: session.user }; - }; - - return Authenticated; -} diff --git a/examples/api-call-example/lib/url-helper.js b/examples/api-call-example/lib/url-helper.js deleted file mode 100644 index 6512fdbc9..000000000 --- a/examples/api-call-example/lib/url-helper.js +++ /dev/null @@ -1,6 +0,0 @@ -export default function createLoginUrl(redirectTo) { - if (redirectTo) { - return `/api/login?redirectTo=${encodeURIComponent(redirectTo)}`; - } - return `/api/login`; -} diff --git a/examples/api-call-example/pages/profile-ssr.jsx b/examples/api-call-example/pages/profile-ssr.jsx index 66246b2f8..365160404 100644 --- a/examples/api-call-example/pages/profile-ssr.jsx +++ b/examples/api-call-example/pages/profile-ssr.jsx @@ -1,7 +1,9 @@ import React from 'react'; +import { withAuth } from '@auth0/nextjs-auth0'; import Layout from '../components/layout'; -import withAuth from '../components/with-auth'; +import LoginRedirect from '../components/login-redirect'; +import auth0 from '../lib/auth0'; const Profile = ({ user }) => ( @@ -14,4 +16,4 @@ const Profile = ({ user }) => ( ); -export default withAuth(Profile); +export default withAuth(Profile, LoginRedirect, auth0); diff --git a/examples/api-call-example/pages/protected-page.jsx b/examples/api-call-example/pages/protected-page.jsx index 9df18ab85..a11b76959 100644 --- a/examples/api-call-example/pages/protected-page.jsx +++ b/examples/api-call-example/pages/protected-page.jsx @@ -1,8 +1,9 @@ import React from 'react'; -import { useUser } from '@auth0/nextjs-auth0'; +import { useUser, withAuth } from '@auth0/nextjs-auth0'; import Layout from '../components/layout'; -import withAuth from '../components/with-auth'; +import LoginRedirect from '../components/login-redirect'; +import auth0 from '../lib/auth0'; export function ProtectedPage() { const { user, loading } = useUser(); @@ -23,4 +24,4 @@ export function ProtectedPage() { ); } -export default withAuth(ProtectedPage); +export default withAuth(ProtectedPage, LoginRedirect, auth0); diff --git a/examples/basic-example/lib/url-helper.js b/examples/basic-example/lib/url-helper.js deleted file mode 100644 index 6512fdbc9..000000000 --- a/examples/basic-example/lib/url-helper.js +++ /dev/null @@ -1,6 +0,0 @@ -export default function createLoginUrl(redirectTo) { - if (redirectTo) { - return `/api/login?redirectTo=${encodeURIComponent(redirectTo)}`; - } - return `/api/login`; -} From 9427c60861ea5327bfe03db727ecd79cf4639d42 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 4 Nov 2020 20:38:47 -0300 Subject: [PATCH 4/5] Update typescript example --- .../typescript-example/components/layout.tsx | 2 +- .../components/login-redirect.tsx | 24 ++++------ .../components/with-auth.tsx | 47 ------------------- examples/typescript-example/lib/url-helper.ts | 6 --- .../typescript-example/pages/profile-ssr.tsx | 7 +-- .../pages/protected-page.tsx | 7 +-- tests/helpers/hooks.tsx | 2 +- 7 files changed, 18 insertions(+), 77 deletions(-) delete mode 100644 examples/typescript-example/components/with-auth.tsx delete mode 100644 examples/typescript-example/lib/url-helper.ts diff --git a/examples/typescript-example/components/layout.tsx b/examples/typescript-example/components/layout.tsx index a0ee016b0..928cc1421 100644 --- a/examples/typescript-example/components/layout.tsx +++ b/examples/typescript-example/components/layout.tsx @@ -3,7 +3,7 @@ import Head from 'next/head'; import Header from './header'; -type LayoutProps = React.PropsWithChildren; +type LayoutProps = React.PropsWithChildren<{}>; const Layout: React.FunctionComponent = ({ children }: LayoutProps) => ( <> diff --git a/examples/typescript-example/components/login-redirect.tsx b/examples/typescript-example/components/login-redirect.tsx index f3f785834..e85f56343 100644 --- a/examples/typescript-example/components/login-redirect.tsx +++ b/examples/typescript-example/components/login-redirect.tsx @@ -1,19 +1,11 @@ -import Router from 'next/router'; -import React, { Component } from 'react'; +import React from 'react'; -import Layout from '../components/layout'; -import createLoginUrl from '../lib/url-helper'; +import Layout from './layout'; -export default class RedirectToLogin extends Component { - componentDidMount(): void { - window.location.assign(createLoginUrl(Router.pathname)); - } +const LoginRedirect = (): React.ReactElement => ( + +
Signing you in...
+
+); - render(): React.ReactElement { - return ( - -
Signing you in...
-
- ); - } -} +export default LoginRedirect; diff --git a/examples/typescript-example/components/with-auth.tsx b/examples/typescript-example/components/with-auth.tsx deleted file mode 100644 index 12e645631..000000000 --- a/examples/typescript-example/components/with-auth.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { NextPage, NextPageContext } from 'next'; -import { UserProfile, useUser } from '@auth0/nextjs-auth0'; - -import auth0 from '../lib/auth0'; -import createLoginUrl from '../lib/url-helper'; -import RedirectToLogin from '../components/login-redirect'; - -type AuthenticatedProps = React.PropsWithChildren<{ user?: UserProfile }>; - -export default function withAuth( - InnerComponent: React.ElementType | React.FunctionComponent -): NextPage { - const Authenticated: NextPage = (props) => { - const { user } = useUser(); - - if (!user) { - return ; // do you need a "redirecting to login" route? - } - - return ; - }; - - Authenticated.getInitialProps = async (context: NextPageContext): Promise => { - if (!context.req) { - const response = await fetch('/api/me'); - const result = response.ok ? await response.json() : null; - - return { user: result, children: undefined }; - } - - const session = await auth0.getSession(context.req, context.res); - - if (!session || !session.user) { - context.res.writeHead(302, { - Location: createLoginUrl(context.req.url) - }); - context.res.end(); - - return; - } - - return { user: session.user, children: undefined }; - }; - - return Authenticated; -} diff --git a/examples/typescript-example/lib/url-helper.ts b/examples/typescript-example/lib/url-helper.ts deleted file mode 100644 index a2b294b78..000000000 --- a/examples/typescript-example/lib/url-helper.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default function createLoginUrl(redirectTo?: string): string { - if (redirectTo) { - return `/api/login?redirectTo=${encodeURIComponent(redirectTo)}`; - } - return `/api/login`; -} diff --git a/examples/typescript-example/pages/profile-ssr.tsx b/examples/typescript-example/pages/profile-ssr.tsx index 5ac9e9c4b..b6d7af33b 100644 --- a/examples/typescript-example/pages/profile-ssr.tsx +++ b/examples/typescript-example/pages/profile-ssr.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import { UserProfile } from '@auth0/nextjs-auth0'; +import { UserProfile, withAuth } from '@auth0/nextjs-auth0'; import Layout from '../components/layout'; -import withAuth from '../components/with-auth'; +import LoginRedirect from '../components/login-redirect'; +import auth0 from '../lib/auth0'; type ProfileProps = { user: UserProfile }; @@ -17,4 +18,4 @@ const Profile = ({ user }: ProfileProps): React.ReactElement => ( ); -export default withAuth(Profile); +export default withAuth(Profile, LoginRedirect, auth0); diff --git a/examples/typescript-example/pages/protected-page.tsx b/examples/typescript-example/pages/protected-page.tsx index 209cf2cbd..1787d03d3 100644 --- a/examples/typescript-example/pages/protected-page.tsx +++ b/examples/typescript-example/pages/protected-page.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import { useUser } from '@auth0/nextjs-auth0'; +import { useUser, withAuth } from '@auth0/nextjs-auth0'; import Layout from '../components/layout'; -import withAuth from '../components/with-auth'; +import LoginRedirect from '../components/login-redirect'; +import auth0 from '../lib/auth0'; export function ProtectedPage(): React.ReactElement { const { user, loading } = useUser(); @@ -23,4 +24,4 @@ export function ProtectedPage(): React.ReactElement { ); } -export default withAuth(ProtectedPage); +export default withAuth(ProtectedPage, LoginRedirect, auth0); diff --git a/tests/helpers/hooks.tsx b/tests/helpers/hooks.tsx index cf793b5fb..5c3fdfb4d 100644 --- a/tests/helpers/hooks.tsx +++ b/tests/helpers/hooks.tsx @@ -17,7 +17,7 @@ export const user: UserProfile = { }; export const withUser = (user: UserProfile | null) => { - return ({ children }: React.PropsWithChildren): React.ReactElement => ( + return ({ children }: React.PropsWithChildren<{}>): React.ReactElement => ( {children} ); }; From 47fd7d51f204b11958f23b49ab0471185465733f Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Thu, 5 Nov 2020 11:08:29 -0300 Subject: [PATCH 5/5] Address review feedback --- src/hooks/with-auth.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/hooks/with-auth.tsx b/src/hooks/with-auth.tsx index 078dea2e3..19d544d84 100644 --- a/src/hooks/with-auth.tsx +++ b/src/hooks/with-auth.tsx @@ -11,10 +11,6 @@ type RedirectToLoginProps = { }; class RedirectToLogin extends Component { - public constructor(props: RedirectToLoginProps) { - super(props); - } - public componentDidMount(): void { window.location.assign(createLoginUrl(Router.pathname)); } @@ -46,7 +42,7 @@ export default function withAuth( const response = await fetch('/api/me'); const result = response.ok ? await response.json() : null; - return { user: result, children: undefined }; + return { user: result }; } const session = await instance.getSession(context.req as NextApiRequest, context.res as NextApiResponse); @@ -57,10 +53,10 @@ export default function withAuth( }); context.res?.end(); - return { user: null, children: undefined }; + return { user: null }; } - return { user: session.user as NullableUserProfile, children: undefined }; + return { user: session.user as NullableUserProfile }; }; return Authenticated;