diff --git a/examples/next/pages/_document.page.tsx b/examples/next/pages/_document.page.tsx index 4122e24f77a..52f07545e8c 100644 --- a/examples/next/pages/_document.page.tsx +++ b/examples/next/pages/_document.page.tsx @@ -20,8 +20,6 @@ class MyDocument extends Document { React Example App -

React Example App

-
diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/StorageBrowser.ts b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/StorageBrowser.ts new file mode 100644 index 00000000000..6b6f2538710 --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/StorageBrowser.ts @@ -0,0 +1,26 @@ +import { Amplify } from 'aws-amplify'; + +import { + createAmplifyAuthAdapter, + createStorageBrowser, + elementsDefault, +} from '@aws-amplify/ui-react-storage/browser'; +import '@aws-amplify/ui-react-storage/styles.css'; +import '@aws-amplify/ui-react-storage/storage-browser-styles.css'; + +import config from './aws-exports'; + +Amplify.configure(config); + +const defaultPrefixes = [ + 'public/', + // intentionally added to test a prefix that should return 403 forbidden + 'forbidden/', + (identityId: string) => `protected/${identityId}/`, + (identityId: string) => `private/${identityId}/`, +]; + +export const { StorageBrowser } = createStorageBrowser({ + elements: elementsDefault, + config: createAmplifyAuthAdapter({ options: { defaultPrefixes } }), +}); diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/[location-detail]/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/[location-detail]/index.page.tsx new file mode 100644 index 00000000000..509b7c40096 --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/[location-detail]/index.page.tsx @@ -0,0 +1,58 @@ +import { useRouter } from 'next/router'; + +import { signOut } from 'aws-amplify/auth'; +import { Button, Flex } from '@aws-amplify/ui-react'; + +import { StorageBrowser } from '../../StorageBrowser'; + +import '@aws-amplify/ui-react-storage/storage-browser-styles.css'; +import '@aws-amplify/ui-react-storage/styles.css'; + +export default function Page() { + const router = useRouter(); + + if (!router.query.bucket) return null; + + return ( + + + + { + router.replace({ query: { ...router.query, actionType } }); + }} + onExit={() => { + router.back(); + }} + /> + {typeof router.query.actionType === 'string' ? ( + + { + router.replace({ + query: { ...router.query, actionType: undefined }, + }); + }} + /> + + ) : null} + + + ); +} diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/index.page.tsx new file mode 100644 index 00000000000..e43bc321b4a --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/[locations]/index.page.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { signOut } from 'aws-amplify/auth'; +import { Button, Flex } from '@aws-amplify/ui-react'; + +import { StorageBrowser } from '../StorageBrowser'; + +import '@aws-amplify/ui-react-storage/storage-browser-styles.css'; +import '@aws-amplify/ui-react-storage/styles.css'; + +function Locations() { + const router = useRouter(); + + return ( + + + + + { + router.push({ + pathname: `${router.pathname}/location-detail`, + query: { ...destination }, + }); + }} + /> + + + + ); +} + +export default Locations; diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/aws-exports.js b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/aws-exports.js new file mode 100644 index 00000000000..4245c81a219 --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/aws-exports.js @@ -0,0 +1,2 @@ +import awsExports from '@environments/storage/file-uploader/src/aws-exports'; +export default awsExports; diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/index.page.tsx new file mode 100644 index 00000000000..9cfc40de58b --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/index.page.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import useIsSignedIn from './useIsSignedIn'; + +import { Authenticator } from '@aws-amplify/ui-react'; + +import '@aws-amplify/ui-react-storage/styles.css'; +import '@aws-amplify/ui-react-storage/storage-browser-styles.css'; + +function Example() { + const router = useRouter(); + + useIsSignedIn({ + onSignIn: () => { + router.push(`${router.pathname}/locations`); + }, + }); + + return ; +} + +export default Example; diff --git a/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/useIsSignedIn.ts b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/useIsSignedIn.ts new file mode 100644 index 00000000000..b3099704ccc --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/default-auth/routed/useIsSignedIn.ts @@ -0,0 +1,71 @@ +import React from 'react'; + +import { fetchAuthSession } from 'aws-amplify/auth'; +import { Hub, HubCallback } from '@aws-amplify/core'; +import { isFunction } from '@aws-amplify/ui'; + +export interface UseIsSignedInParams { + onSignIn?: () => void; + onSignOut?: () => void; +} + +interface UseIsSignedIn { + isSignedIn: boolean; +} + +const INITIAL_STATE: UseIsSignedIn = { isSignedIn: false }; + +/** + * listens for `Auth` sign in and sign out events + * + * @param {UseIsSignedInParams} params `onSignIn` and `onSignOut` event callbacks + * @returns {UseIsSignedIn} Outputs `isSignedIn` + */ +export default function useIsSignedIn({ + onSignIn, + onSignOut, +}: UseIsSignedInParams): UseIsSignedIn { + const [output, setOutput] = React.useState( + () => INITIAL_STATE + ); + + React.useEffect(() => { + fetchAuthSession().then(() => { + if (isFunction(onSignIn)) onSignIn(); + }); + }, [onSignIn]); + + const handleEvent: HubCallback = React.useCallback( + ({ payload }) => { + switch (payload.event) { + case 'signIn': + case 'autoSignIn': { + if (isFunction(onSignIn)) { + onSignIn(); + } + setOutput({ isSignedIn: true }); + break; + } + case 'signOut': { + if (isFunction(onSignOut)) { + onSignOut(); + } + setOutput({ isSignedIn: false }); + break; + } + default: { + break; + } + } + }, + [onSignIn, onSignOut] + ); + + React.useEffect(() => { + const unsubscribe = Hub.listen('auth', handleEvent, 'useIsSignedIn'); + + return unsubscribe; + }, [handleEvent]); + + return output; +} diff --git a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/index.page.tsx index b8e23faef8e..8ae7e54e41a 100644 --- a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/index.page.tsx +++ b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/index.page.tsx @@ -2,9 +2,8 @@ import React from 'react'; import { createStorageBrowser } from '@aws-amplify/ui-react-storage/browser'; -import { auth, managedAuthAdapter } from '../managedAuthAdapter'; - -import { Button, Flex } from '@aws-amplify/ui-react'; +import { managedAuthAdapter } from '../managedAuthAdapter'; +import { SignIn, SignOutButton } from './routed/components'; import '@aws-amplify/ui-react-storage/storage-browser-styles.css'; import '@aws-amplify/ui-react-storage/styles.css'; @@ -12,43 +11,13 @@ import '@aws-amplify/ui-react-storage/styles.css'; const { StorageBrowser } = createStorageBrowser({ config: managedAuthAdapter }); function Example() { - const [authenticated, setAuthenticated] = React.useState(false); - const [isLoading, setIsLoading] = React.useState(false); - const [errorMessage, setErrorMessage] = React.useState(); + const [showSignIn, setShowSignIn] = React.useState(false); - return !authenticated ? ( - - - {isLoading ? Authenticating... : null} - {errorMessage ? {errorMessage} : null} - + return !showSignIn ? ( + setShowSignIn(true)} /> ) : ( <> - + setShowSignIn(false)} /> ); diff --git a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/StorageBrowser.ts b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/StorageBrowser.ts new file mode 100644 index 00000000000..89a3b4d478b --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/StorageBrowser.ts @@ -0,0 +1,16 @@ +import { Auth } from '../../managedAuthAdapter'; +import { + createManagedAuthAdapter, + createStorageBrowser, +} from '@aws-amplify/ui-react-storage/browser'; + +export const routedAuth = new Auth({ persistCredentials: true }); + +const config = createManagedAuthAdapter({ + credentialsProvider: routedAuth.credentialsProvider, + region: process.env.NEXT_PUBLIC_MANAGED_AUTH_REGION, + accountId: process.env.NEXT_PUBLIC_MANAGED_AUTH_ACCOUNT_ID, + registerAuthListener: routedAuth.registerAuthListener, +}); + +export const { StorageBrowser } = createStorageBrowser({ config }); diff --git a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/[locations]/[location-detail]/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/[locations]/[location-detail]/index.page.tsx new file mode 100644 index 00000000000..d590ed7279e --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/[locations]/[location-detail]/index.page.tsx @@ -0,0 +1,52 @@ +import { useRouter } from 'next/router'; + +import { Flex } from '@aws-amplify/ui-react'; + +import { SignOutButton } from '../../components'; +import { StorageBrowser } from '../../StorageBrowser'; + +import '@aws-amplify/ui-react-storage/storage-browser-styles.css'; +import '@aws-amplify/ui-react-storage/styles.css'; + +export default function Page() { + const { back, query, pathname, replace } = useRouter(); + + if (!query.bucket) return null; + + return ( + + { + replace(pathname.replace('[locations]/[location-detail]', '')); + }} + /> + + { + replace({ query: { ...query, actionType } }); + }} + onNavigate={(destination) => { + replace({ query: { ...destination } }); + }} + onExit={() => { + back(); + }} + /> + {typeof query.actionType === 'string' ? ( + + { + replace({ query: { ...query, actionType: undefined } }); + }} + /> + + ) : null} + + + ); +} diff --git a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/[locations]/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/[locations]/index.page.tsx new file mode 100644 index 00000000000..60906f26802 --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/[locations]/index.page.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { Flex } from '@aws-amplify/ui-react'; + +import { SignOutButton } from '../components'; +import { StorageBrowser } from '../StorageBrowser'; + +import '@aws-amplify/ui-react-storage/storage-browser-styles.css'; +import '@aws-amplify/ui-react-storage/styles.css'; + +function Locations() { + const router = useRouter(); + + return ( + + { + router.replace(router.pathname.replace('[locations]', '')); + }} + /> + + + { + router.push({ + pathname: `${router.pathname}/location-detail`, + query: { ...destination }, + }); + }} + /> + + + ); +} + +export default Locations; diff --git a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/components.tsx b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/components.tsx new file mode 100644 index 00000000000..62672c7ce3c --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/components.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import { Button, Flex } from '@aws-amplify/ui-react'; + +import { routedAuth } from './StorageBrowser'; + +export function SignIn({ onSignIn }: { onSignIn?: () => void }) { + const [isLoading, setIsLoading] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(); + + return ( + + + + {isLoading ? Authenticating... : null} + {errorMessage ? {errorMessage} : null} + + ); +} + +export function SignOutButton({ onSignOut }: { onSignOut?: () => void }) { + return ( + + ); +} diff --git a/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/index.page.tsx b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/index.page.tsx new file mode 100644 index 00000000000..39451335171 --- /dev/null +++ b/examples/next/pages/ui/components/storage/storage-browser/managed-auth/routed/index.page.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import { SignIn } from './components'; + +import '@aws-amplify/ui-react-storage/storage-browser-styles.css'; +import '@aws-amplify/ui-react-storage/styles.css'; + +function Example() { + const router = useRouter(); + + return ( + { + router.push(`${router.pathname}/locations`); + }} + /> + ); +} + +export default Example; diff --git a/examples/next/pages/ui/components/storage/storage-browser/managedAuthAdapter.ts b/examples/next/pages/ui/components/storage/storage-browser/managedAuthAdapter.ts index d67cb457254..f6f8051c1df 100644 --- a/examples/next/pages/ui/components/storage/storage-browser/managedAuthAdapter.ts +++ b/examples/next/pages/ui/components/storage/storage-browser/managedAuthAdapter.ts @@ -6,21 +6,48 @@ import { type CredentialsProvider = CreateManagedAuthAdapterInput['credentialsProvider']; type Credentials = Awaited>; -class Auth { +export class Auth { + #persistCredentials: boolean; #credentials: Credentials | undefined; #onAuthStatusChange: () => void | undefined; + constructor(options?: { persistCredentials?: boolean }) { + const { persistCredentials = false } = options ?? {}; + this.#persistCredentials = persistCredentials; + } + + #clearCredentials() { + this.#onAuthStatusChange?.(); + this.#onAuthStatusChange = undefined; + localStorage.removeItem('creds'); + this.#credentials = undefined; + } + #getCredentials(): Credentials { + if (this.#persistCredentials) { + return JSON.parse(localStorage.getItem('creds')); + } + return this.#credentials; } #setCredentials(credentials: Credentials) { + if (this.#persistCredentials) { + localStorage.setItem('creds', JSON.stringify(credentials)); + } this.#credentials = credentials; } - async #fetchCredentials(): Promise { + async #fetchCredentials(options?: { + forceRefresh?: boolean; + }): Promise { + const { forceRefresh = false } = options ?? {}; + + if (forceRefresh) { + this.#clearCredentials(); + } const credentials = this.#getCredentials(); - if (credentials) { + if (!forceRefresh && credentials) { return credentials; } @@ -53,7 +80,7 @@ class Auth { } get credentialsProvider(): CredentialsProvider { - return async () => await this.#fetchCredentials(); + return (options) => this.#fetchCredentials(options); } registerAuthListener = (onAuthStatusChange: () => void) => { @@ -61,12 +88,13 @@ class Auth { }; async signIn(input?: { + forceRefresh?: boolean; onSignIn?: () => void; onError?: (e: Error) => void; }): Promise { - const { onError, onSignIn } = input ?? {}; + const { forceRefresh, onError, onSignIn } = input ?? {}; try { - await this.#fetchCredentials(); + await this.#fetchCredentials({ forceRefresh }); onSignIn?.(); } catch (e) { onError?.(e); @@ -74,9 +102,8 @@ class Auth { } signOut(input?: { onSignOut?: () => void }) { - this.#credentials = undefined; - this.#onAuthStatusChange?.(); - input?.onSignOut(); + this.#clearCredentials(); + input?.onSignOut?.(); } }