diff --git a/reactjs-todo-dv/.env.example b/reactjs-todo-dv/.env.example new file mode 100644 index 0000000..2970fb0 --- /dev/null +++ b/reactjs-todo-dv/.env.example @@ -0,0 +1,8 @@ +API_URL=$API_URL +DEBUGGER_OFF=true +DEVELOPMENT=$DEVELOPMENT +PORT=$PORT +CLIENT_ID=$CLIENT_ID +REDIRECT_URI=$REDIRECT_URI +SCOPE="openid profile email phone" +BASE_URL=$BASE_URL \ No newline at end of file diff --git a/reactjs-todo-dv/.eslintrc.js b/reactjs-todo-dv/.eslintrc.js new file mode 100644 index 0000000..f817415 --- /dev/null +++ b/reactjs-todo-dv/.eslintrc.js @@ -0,0 +1,39 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + ignorePatterns: [ + '**/node_modules/**', + '**/dist/**', + 'public', + 'playwright-report', + 'test-results', + ], + extends: ['standard', 'plugin:react/recommended', 'prettier'], + overrides: [ + { + env: { + node: true, + }, + files: ['.eslintrc.{js,cjs}'], + parserOptions: { + sourceType: 'script', + }, + }, + ], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['react'], + settings: { + react: { + version: 'detect', + }, + }, + rules: { + 'react/prop-types': 'off', + 'no-debugger': 'off', + }, +}; diff --git a/reactjs-todo-dv/.gitignore b/reactjs-todo-dv/.gitignore new file mode 100644 index 0000000..f8048ca --- /dev/null +++ b/reactjs-todo-dv/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/reactjs-todo-dv/README.md b/reactjs-todo-dv/README.md new file mode 100644 index 0000000..0a0188d --- /dev/null +++ b/reactjs-todo-dv/README.md @@ -0,0 +1,117 @@ +# React JS Todo Sample App + +## Disclaimers + +This sample code is provided "as is" and is not a supported product of ForgeRock. It's purpose is solely to demonstrate how the ForgeRock JavaScript SDK can be implemented within a React application. Also, this is not a demonstration of React itself or instructional for _how_ to build a React app. There are many aspects to routing, state management, tooling and other aspects to building a React app that are outside of the scope of this project. For information about creating a React app, [visit React's official documentation](https://reactjs.org/docs/create-a-new-react-app.html). + +## Requirements + +1. A PingOne tenant with SSO and DaVinci services enabled +2. Node >= 14.2.0 (recommended: install via [official package installer](https://nodejs.org/en/)) +3. Knowledge of using the Terminal/Command Line +4. This project "cloned" to your computer + +## Setup + +Once you have the requirements above met, we can build the project. + +### Setup Your PingOne application + +1. Create a new OIDC Web App + +#### Configuration + +1. CORS Allowed origins: `http://localhost:8443` +2. Token Auth Method: None +3. Signoff URLs: http://localhost:8443/login +4. Redirect URIs: http://localhost:8443/login +5. Response Type: Code +6. Grant Type: Authorization Code + +#### Resources (scopes) + +1. email phone profile + +#### Policies + +1. DaVinci Policies: Select your DaVinci application + +### Configure Your `.env` File + +Change the name of `.env.example` to `.env` and replace the bracketed values (e.g. `<<>>`) with your values. + +Example with annotations: + +```text +API_URL=http://localhost:9443 +DEBUGGER_OFF=false +DEVELOPMENT=true +PORT=8443 +CLIENT_ID=<> +REDIRECT_URI=http://localhost:8443/login +SCOPE="openid profile email phone" +BASE_URL=https://auth.pingone.com/<>/ +``` + +### Installing Dependencies and Run Build + +#### NOTE:a new workspace for this sample app has not been added at this point. TODO: update before releasing +#### Todo Api was modified to work with PingOne endpoints and token properties. Those modifications are not yet in this repo. + +**Run from root of repo**: since this sample app uses npm's workspaces, we recommend running the npm commands from the root of the repo. + +```sh +# Install all dependencies (no need to pass the -w option) +npm install +``` + +### Run the Servers + +Now, run the below commands to start the processes needed for building the application and running the servers for both client and API server: + +```sh +# In one terminal window, run the following watch command from the root of the repository +npm run start:reactjs-todo +``` + +Now, you should be able to visit `http://localhost:8443`, which is your web app or client (the Relying Party in OAuth terms). This client will make requests to your AM instance, (the Authorization Server in OAuth terms), which will be running on whatever domain you set, and `http://localhost:9443` as the REST API for your todos (the Resource Server). + +## Learn About Integration Touchpoints + +This project has a debugging statements that can be activated which causes the app to pause execution at each SDK integration point. It will have a comment above the `debugger` statement explaining the purpose of the integration. + +If you'd like to use this feature as a learning tool, [open the live app](https://fr-react-todos.crbrl.io/) and then open the developer tools of your browser. Rerun the app with the developer tools open, and it will automatically pause at these points of integration. + +For local development, if you want to turn these debuggers off, you can set the environment variable of `DEBUGGER_OFF` to true. + +## Modifying This Project + +### React Client + +To modify the client portion of this project, you'll need to be familiar with the following React patterns: + +1. [Functional components and composition](https://reactjs.org/docs/components-and-props.html) +2. [Hooks (including custom hooks)](https://reactjs.org/docs/hooks-intro.html) +3. [Context API](https://reactjs.org/docs/hooks-reference.html#usecontext) +4. [React Router](https://reactrouter.com/) + +You'll also want a [basic understanding of Webpack](https://webpack.js.org/concepts/) and the following: + +1. [Babel transformation for React](https://webpack.js.org/loaders/babel-loader/#root) +2. [Plugins for Sass-to-CSS processing](https://webpack.js.org/loaders/sass-loader/#root) + +#### Styling and CSS + +We heavily leveraged [Twitter Bootstrap](https://getbootstrap.com/) and [it's utility classes](https://getbootstrap.com/docs/5.0/utilities/api/), but you will see classes with the prefix `cstm_`. These are custom classes, hence the `cstm` shorthand, and they are explicitly used to denote an additional style application on top of Bootstrap's styling. + +### REST API Server + +To modify the API server, you'll need a [basic understanding of Node](https://nodejs.org/en/about/) as well as the following things: + +1. [Express](https://expressjs.com/) +2. [PouchDB](https://pouchdb.com/) +3. [Superagent](https://www.npmjs.com/package/superagent) + +## TypeScript? + +The ForgeRock Javascript SDK is developed with TypeScript, so type definitions are available. This sample application does not utilize TypeScript, but if you'd like to see a version of this written in TypeScript, let us know. diff --git a/reactjs-todo-dv/babel.config.js b/reactjs-todo-dv/babel.config.js new file mode 100644 index 0000000..463de36 --- /dev/null +++ b/reactjs-todo-dv/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['@babel/preset-react'], +}; diff --git a/reactjs-todo-dv/client/README.md b/reactjs-todo-dv/client/README.md new file mode 100644 index 0000000..0a0188d --- /dev/null +++ b/reactjs-todo-dv/client/README.md @@ -0,0 +1,117 @@ +# React JS Todo Sample App + +## Disclaimers + +This sample code is provided "as is" and is not a supported product of ForgeRock. It's purpose is solely to demonstrate how the ForgeRock JavaScript SDK can be implemented within a React application. Also, this is not a demonstration of React itself or instructional for _how_ to build a React app. There are many aspects to routing, state management, tooling and other aspects to building a React app that are outside of the scope of this project. For information about creating a React app, [visit React's official documentation](https://reactjs.org/docs/create-a-new-react-app.html). + +## Requirements + +1. A PingOne tenant with SSO and DaVinci services enabled +2. Node >= 14.2.0 (recommended: install via [official package installer](https://nodejs.org/en/)) +3. Knowledge of using the Terminal/Command Line +4. This project "cloned" to your computer + +## Setup + +Once you have the requirements above met, we can build the project. + +### Setup Your PingOne application + +1. Create a new OIDC Web App + +#### Configuration + +1. CORS Allowed origins: `http://localhost:8443` +2. Token Auth Method: None +3. Signoff URLs: http://localhost:8443/login +4. Redirect URIs: http://localhost:8443/login +5. Response Type: Code +6. Grant Type: Authorization Code + +#### Resources (scopes) + +1. email phone profile + +#### Policies + +1. DaVinci Policies: Select your DaVinci application + +### Configure Your `.env` File + +Change the name of `.env.example` to `.env` and replace the bracketed values (e.g. `<<>>`) with your values. + +Example with annotations: + +```text +API_URL=http://localhost:9443 +DEBUGGER_OFF=false +DEVELOPMENT=true +PORT=8443 +CLIENT_ID=<> +REDIRECT_URI=http://localhost:8443/login +SCOPE="openid profile email phone" +BASE_URL=https://auth.pingone.com/<>/ +``` + +### Installing Dependencies and Run Build + +#### NOTE:a new workspace for this sample app has not been added at this point. TODO: update before releasing +#### Todo Api was modified to work with PingOne endpoints and token properties. Those modifications are not yet in this repo. + +**Run from root of repo**: since this sample app uses npm's workspaces, we recommend running the npm commands from the root of the repo. + +```sh +# Install all dependencies (no need to pass the -w option) +npm install +``` + +### Run the Servers + +Now, run the below commands to start the processes needed for building the application and running the servers for both client and API server: + +```sh +# In one terminal window, run the following watch command from the root of the repository +npm run start:reactjs-todo +``` + +Now, you should be able to visit `http://localhost:8443`, which is your web app or client (the Relying Party in OAuth terms). This client will make requests to your AM instance, (the Authorization Server in OAuth terms), which will be running on whatever domain you set, and `http://localhost:9443` as the REST API for your todos (the Resource Server). + +## Learn About Integration Touchpoints + +This project has a debugging statements that can be activated which causes the app to pause execution at each SDK integration point. It will have a comment above the `debugger` statement explaining the purpose of the integration. + +If you'd like to use this feature as a learning tool, [open the live app](https://fr-react-todos.crbrl.io/) and then open the developer tools of your browser. Rerun the app with the developer tools open, and it will automatically pause at these points of integration. + +For local development, if you want to turn these debuggers off, you can set the environment variable of `DEBUGGER_OFF` to true. + +## Modifying This Project + +### React Client + +To modify the client portion of this project, you'll need to be familiar with the following React patterns: + +1. [Functional components and composition](https://reactjs.org/docs/components-and-props.html) +2. [Hooks (including custom hooks)](https://reactjs.org/docs/hooks-intro.html) +3. [Context API](https://reactjs.org/docs/hooks-reference.html#usecontext) +4. [React Router](https://reactrouter.com/) + +You'll also want a [basic understanding of Webpack](https://webpack.js.org/concepts/) and the following: + +1. [Babel transformation for React](https://webpack.js.org/loaders/babel-loader/#root) +2. [Plugins for Sass-to-CSS processing](https://webpack.js.org/loaders/sass-loader/#root) + +#### Styling and CSS + +We heavily leveraged [Twitter Bootstrap](https://getbootstrap.com/) and [it's utility classes](https://getbootstrap.com/docs/5.0/utilities/api/), but you will see classes with the prefix `cstm_`. These are custom classes, hence the `cstm` shorthand, and they are explicitly used to denote an additional style application on top of Bootstrap's styling. + +### REST API Server + +To modify the API server, you'll need a [basic understanding of Node](https://nodejs.org/en/about/) as well as the following things: + +1. [Express](https://expressjs.com/) +2. [PouchDB](https://pouchdb.com/) +3. [Superagent](https://www.npmjs.com/package/superagent) + +## TypeScript? + +The ForgeRock Javascript SDK is developed with TypeScript, so type definitions are available. This sample application does not utilize TypeScript, but if you'd like to see a version of this written in TypeScript, let us know. diff --git a/reactjs-todo-dv/client/components/README.md b/reactjs-todo-dv/client/components/README.md new file mode 100644 index 0000000..45805b6 --- /dev/null +++ b/reactjs-todo-dv/client/components/README.md @@ -0,0 +1,3 @@ +# Components + +These are React based units of code that could potentially be used anywhere, to an extent. They are the units that compose a view. These could be actual React components or custom React hooks. diff --git a/reactjs-todo-dv/client/components/davinci-client/davinci-flow.js b/reactjs-todo-dv/client/components/davinci-client/davinci-flow.js new file mode 100644 index 0000000..4114175 --- /dev/null +++ b/reactjs-todo-dv/client/components/davinci-client/davinci-flow.js @@ -0,0 +1,163 @@ +/* + * forgerock-sample-web-react + * + * davinci-flow.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import React, { useContext, useEffect, useState, useRef } from 'react'; +import { TokenManager, UserManager } from '@forgerock/javascript-sdk'; + +import TextInput from './text-input.js'; +import Password from './password.js'; +import SubmitButton from './submit-button.js'; +import Protect from './protect.js'; +import FlowButton from './flow-button.js'; +import ErrorMessage from './error-message.js'; +import { AppContext } from '../../global-state.js'; + +/** + * @function DaVinciFlow - React view for a DaVinci flow + * @returns {Object} - React component object + */ +export default function DaVinciFlow({ davinciClient, flowCompleteCb }) { + /** + * Collects the global state for detecting user auth for rendering + * appropriate navigational items. + * The destructing of the hook's array results in index 0 having the state value, + * and index 1 having the "setter" method to set new state values. + */ + const [, methods] = useContext(AppContext); + const [collectors, setCollectors] = useState([]); + const [pageHeader, setPageHeader] = useState(''); + const [isSubmittingForm, setIsSubmittingForm] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + useEffect(() => { + (async () => { + const node = await davinciClient.start(); + + if (node.status !== 'success') { + renderForm(node); + } else { + completeFlow(node); + } + })(); + }, []); + + const onSubmitHandler = async (event) => { + event.preventDefault(); + setIsSubmittingForm(true); + /** + * We can just call `next` here and not worry about passing any arguments + */ + const nextNode = await davinciClient.next(); + /** + * Recursively render the form with the new state + */ + mapRenderer(nextNode); + }; + + async function completeFlow(successNode) { + const clientInfo = davinciClient.getClient(); + + let code = ''; + let state = ''; + + if (clientInfo?.status === 'success') { + code = clientInfo.authorization?.code || ''; + state = clientInfo.authorization?.state || ''; + } + + await TokenManager.getTokens({ query: { code, state } }); + const user = await UserManager.getCurrentUser(); + methods.setUser(user.preferred_username); + methods.setEmail(user.email); + methods.setAuthentication(true); + // Login flow specific callback + flowCompleteCb(); + } + + // Update the UI with the new node + async function renderForm(nextNode) { + // clear form contents + setCollectors([]); + // Set h1 header + setPageHeader(nextNode.client?.name || ''); + const collectors = davinciClient.getCollectors(); + // Save collectors to state + setCollectors(collectors); + // If node is a protect node, move to next node without user interaction + if (davinciClient.getCollectors().find((collector) => collector.name === 'protectsdk')) { + const nextNode = await davinciClient.next(); + mapRenderer(nextNode); + } + } + + function mapRenderer(nextNode) { + setIsSubmittingForm(false); + if (nextNode.status === 'next') { + renderForm(nextNode); + } else if (nextNode.status === 'success') { + completeFlow(nextNode); + } else if (nextNode.status === 'error') { + setErrorMessage(nextNode.error.message); + } else { + console.error('Unknown node status', nextNode); + } + } + + return ( +
+ {pageHeader &&

{pageHeader}

} + {errorMessage.length > 0 && } + {errorMessage.length == 0 && + collectors.map((collector) => { + if (collector.type === 'TextCollector' && collector.name === 'protectsdk') { + return ( + + ); + } else if (collector.type === 'TextCollector') { + return ( + + ); + } else if (collector.type === 'PasswordCollector') { + return ( + + ); + } else if (collector.type === 'SubmitCollector') { + return ( + + ); + } else if (collector.type === 'FlowCollector') { + return ( + + ); + } + })} + + ); +} diff --git a/reactjs-todo-dv/client/components/davinci-client/error-message.js b/reactjs-todo-dv/client/components/davinci-client/error-message.js new file mode 100644 index 0000000..d7f4fcb --- /dev/null +++ b/reactjs-todo-dv/client/components/davinci-client/error-message.js @@ -0,0 +1,20 @@ +/* + * forgerock-sample-web-react + * + * error-message.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import React from 'react'; +import AlertIcon from '../icons/alert-icon'; + +export default function ErrorMessage({ message }) { + return ( +

+ + {message} +

+ ); +} diff --git a/reactjs-todo-dv/client/components/davinci-client/flow-button.js b/reactjs-todo-dv/client/components/davinci-client/flow-button.js new file mode 100644 index 0000000..ed1f283 --- /dev/null +++ b/reactjs-todo-dv/client/components/davinci-client/flow-button.js @@ -0,0 +1,29 @@ +/* + * forgerock-sample-web-react + * + * flow-button.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import React from 'react'; + +export default function FlowButton({ collector, flow, renderForm }) { + + const clickHandler = async () => { + const node = await flow(collector.output.key); + renderForm(node); + }; + + return ( + + ); +} diff --git a/reactjs-todo-dv/client/components/davinci-client/password.js b/reactjs-todo-dv/client/components/davinci-client/password.js new file mode 100644 index 0000000..ae586dd --- /dev/null +++ b/reactjs-todo-dv/client/components/davinci-client/password.js @@ -0,0 +1,53 @@ +/* + * forgerock-sample-web-react + * + * password.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import React, { useState, useContext } from 'react'; +import { AppContext } from '../../global-state'; +import EyeIcon from '../icons/eye-icon'; + +const Password = ({ collector, updater }) => { + const [appState, methods] = useContext(AppContext); + const [isVisible, setVisibility] = useState(false); + + /** + * @function toggleVisibility - toggles the password from masked to plaintext + */ + function toggleVisibility() { + setVisibility(!isVisible); + } + + return ( +
+ updater(e.target.value)} + key={collector.output.key} + /> + + +
+ ); +}; + +export default Password; diff --git a/reactjs-todo-dv/client/components/davinci-client/protect.js b/reactjs-todo-dv/client/components/davinci-client/protect.js new file mode 100644 index 0000000..b8a7587 --- /dev/null +++ b/reactjs-todo-dv/client/components/davinci-client/protect.js @@ -0,0 +1,15 @@ +/* + * forgerock-sample-web-react + * + * protect.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import React from 'react'; + +export default function ({ collector, updater }) { + updater('fakeprofile'); + return

{collector.output.label}

; +} diff --git a/reactjs-todo-dv/client/components/davinci-client/submit-button.js b/reactjs-todo-dv/client/components/davinci-client/submit-button.js new file mode 100644 index 0000000..d1ac68b --- /dev/null +++ b/reactjs-todo-dv/client/components/davinci-client/submit-button.js @@ -0,0 +1,36 @@ +/* + * forgerock-sample-web-react + * + * submit-button.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import React from 'react'; + +export default function SubmitButton({ collector, submittingForm }) { + return ( + + ); +} diff --git a/reactjs-todo-dv/client/components/davinci-client/text-input.js b/reactjs-todo-dv/client/components/davinci-client/text-input.js new file mode 100644 index 0000000..07db824 --- /dev/null +++ b/reactjs-todo-dv/client/components/davinci-client/text-input.js @@ -0,0 +1,33 @@ +/* + * forgerock-sample-web-react + * + * text-input.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import React, { useContext } from 'react'; +import { AppContext } from '../../global-state'; + +const Text = ({ collector, updater }) => { + const [appState, methods] = useContext(AppContext); + + return ( +
+ updater(e.target.value)} + type="text" + key={collector.output.key} + /> + +
+ ); +}; + +export default Text; diff --git a/reactjs-todo-dv/client/components/icons/account-icon.js b/reactjs-todo-dv/client/components/icons/account-icon.js new file mode 100644 index 0000000..6d20fae --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/account-icon.js @@ -0,0 +1,33 @@ +/* + * forgerock-sample-web-react + * + * account-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function AccountIcon - React component for the user icon representing the account + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function AccountIcon({ classes = '', size = '24px' }) { + return ( + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/action-icon.js b/reactjs-todo-dv/client/components/icons/action-icon.js new file mode 100644 index 0000000..fc6bc32 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/action-icon.js @@ -0,0 +1,33 @@ +/* + * forgerock-sample-web-react + * + * action-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function ActionIcon - React component that displays the action, "three dots" icon representing the a menu + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function ActionIcon({ classes = '', size = '24px' }) { + return ( + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/alert-icon.js b/reactjs-todo-dv/client/components/icons/alert-icon.js new file mode 100644 index 0000000..8a13952 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/alert-icon.js @@ -0,0 +1,33 @@ +/* + * forgerock-sample-web-react + * + * alert-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function AlertIcon - React component that displays the alert icon representing the a warning + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function AlertIcon({ classes = '', size = '24px' }) { + return ( + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/apple-icon.js b/reactjs-todo-dv/client/components/icons/apple-icon.js new file mode 100644 index 0000000..b6495a0 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/apple-icon.js @@ -0,0 +1,40 @@ +/* + * forgerock-sample-web-react + * + * apple-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function AppleIcon - React component for the user icon representing the apple icon + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function AppleIcon({ classes = '', size = '24px' }) { + return ( + + + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/eye-icon.js b/reactjs-todo-dv/client/components/icons/eye-icon.js new file mode 100644 index 0000000..37d7904 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/eye-icon.js @@ -0,0 +1,48 @@ +/* + * forgerock-sample-web-react + * + * eye-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function AlertIcon - React component that displays the eye (password visible) icon + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function EyeIcon({ classes = '', visible = true, size = '24px' }) { + if (visible) { + return ( + + + + + ); + } else { + return ( + + + + + ); + } +} diff --git a/reactjs-todo-dv/client/components/icons/finger-print-icon.js b/reactjs-todo-dv/client/components/icons/finger-print-icon.js new file mode 100644 index 0000000..25808c5 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/finger-print-icon.js @@ -0,0 +1,36 @@ +/* + * forgerock-sample-web-react + * + * finger-print-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function FingerPrintIcon - React component that displays the finger print icon representing login + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function FingerPrintIcon({ classes = '', size = '24px' }) { + return ( + + + + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/forgerock-icon.js b/reactjs-todo-dv/client/components/icons/forgerock-icon.js new file mode 100644 index 0000000..47d0871 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/forgerock-icon.js @@ -0,0 +1,43 @@ +/* + * forgerock-sample-web-react + * + * forgerock-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function ForgeRockIcon - React component that displays the ForgeRock brand icon + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function ForgeRockIcon({ classes = '', size = '24px' }) { + return ( + + + + + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/google-icon.js b/reactjs-todo-dv/client/components/icons/google-icon.js new file mode 100644 index 0000000..1f327b8 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/google-icon.js @@ -0,0 +1,47 @@ +/* + * forgerock-sample-web-react + * + * google-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function GoogleIcon - React component for the user icon representing the google icon + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function GoogleIcon({ classes = '', size = '24px' }) { + return ( + + + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/home-icon.js b/reactjs-todo-dv/client/components/icons/home-icon.js new file mode 100644 index 0000000..94032c6 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/home-icon.js @@ -0,0 +1,33 @@ +/* + * forgerock-sample-web-react + * + * home-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function HomeIcon - Displays the home icon representing the home page + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function HomeIcon({ classes = '', size = '24px' }) { + return ( + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/key-icon.js b/reactjs-todo-dv/client/components/icons/key-icon.js new file mode 100644 index 0000000..83fee9b --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/key-icon.js @@ -0,0 +1,33 @@ +/* + * forgerock-sample-web-react + * + * key-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function KeyIcon - React component that displays the key icon representing login + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function KeyIcon({ classes = '', size = '24px' }) { + return ( + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/left-arrow-icon.js b/reactjs-todo-dv/client/components/icons/left-arrow-icon.js new file mode 100644 index 0000000..c4ae5f3 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/left-arrow-icon.js @@ -0,0 +1,33 @@ +/* + * forgerock-sample-web-react + * + * left-arrow-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function LeftArrowIcon - React component that displays the left arrow representing "back" + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function LeftArrowIcon({ classes = '', size = '24px' }) { + return ( + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/lock-icon.js b/reactjs-todo-dv/client/components/icons/lock-icon.js new file mode 100644 index 0000000..a2a23c1 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/lock-icon.js @@ -0,0 +1,33 @@ +/* + * forgerock-sample-web-react + * + * lock-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function LockIcon - React component that displays the lock icon representing security + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function LockIcon({ classes = '', size = '24px' }) { + return ( + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/new-user-icon.js b/reactjs-todo-dv/client/components/icons/new-user-icon.js new file mode 100644 index 0000000..b4ac8ac --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/new-user-icon.js @@ -0,0 +1,33 @@ +/* + * forgerock-sample-web-react + * + * new-user-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function NewUserIcon - React component that displays the new user (user with +) icon representing registration + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function NewUserIcon({ classes = '', size = '24px' }) { + return ( + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/react-icon.js b/reactjs-todo-dv/client/components/icons/react-icon.js new file mode 100644 index 0000000..5e30cdd --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/react-icon.js @@ -0,0 +1,32 @@ +/* + * forgerock-sample-web-react + * + * react-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function ReactIcon - React component that displays the React brand icon + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function ReactIcon({ classes = '', size = '24px' }) { + return ( + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/todo-icon.js b/reactjs-todo-dv/client/components/icons/todo-icon.js new file mode 100644 index 0000000..2fed385 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/todo-icon.js @@ -0,0 +1,48 @@ +/* + * forgerock-sample-web-react + * + * todo-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function TodoIcon - React component that displays either the circle or circle with check icon representing the todo + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function TodoIcon({ classes, completed, size }) { + if (completed) { + return ( + + + + + ); + } else { + return ( + + + + + ); + } +} diff --git a/reactjs-todo-dv/client/components/icons/todos-icon.js b/reactjs-todo-dv/client/components/icons/todos-icon.js new file mode 100644 index 0000000..14d1960 --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/todos-icon.js @@ -0,0 +1,33 @@ +/* + * forgerock-sample-web-react + * + * todos-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function TodosIcon - React component that displays the multiple checks icon representing the todos page + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function TodosIcon({ classes = '', size = '24px' }) { + return ( + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/icons/verified-icon.js b/reactjs-todo-dv/client/components/icons/verified-icon.js new file mode 100644 index 0000000..37cc1bc --- /dev/null +++ b/reactjs-todo-dv/client/components/icons/verified-icon.js @@ -0,0 +1,37 @@ +/* + * forgerock-sample-web-react + * + * verified-icon.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React from 'react'; + +/** + * @function VerifiedIcon - React component that displays the verified checkmark icon representing the validation + * @param {Object} props - React props object + * @param {string} props.classes - A string of classnames to be set on component + * @param {string} props.size - A string representing the intended size of the rendering + * @returns {Object} - React JSX Object + */ +export default function VerifiedIcon({ classes = '', size = '24px' }) { + return ( + + + + + + + + + ); +} diff --git a/reactjs-todo-dv/client/components/layout/card.js b/reactjs-todo-dv/client/components/layout/card.js new file mode 100644 index 0000000..59f2807 --- /dev/null +++ b/reactjs-todo-dv/client/components/layout/card.js @@ -0,0 +1,31 @@ +/* + * forgerock-sample-web-react + * + * card.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useContext } from 'react'; + +import { AppContext } from '../../global-state'; + +/** + * @function Card - React component that displays the alert icon representing the a warning + * @param {Object} props - React props object + * @param {Object} props.children - The child React components that are passed in + * @returns {Object} - React JSX Object + */ +export default function Card(props) { + const [state] = useContext(AppContext); + + return ( +
+ {props.children} +
+ ); +} diff --git a/reactjs-todo-dv/client/components/layout/footer.js b/reactjs-todo-dv/client/components/layout/footer.js new file mode 100644 index 0000000..d82af53 --- /dev/null +++ b/reactjs-todo-dv/client/components/layout/footer.js @@ -0,0 +1,32 @@ +/* + * forgerock-sample-web-react + * + * footer.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useContext } from 'react'; + +import { AppContext } from '../../global-state'; + +/** + * @function Footer - Footer React component + * @returns {Object} - React component object + */ +export default function Footer() { + const [state] = useContext(AppContext); + + return ( +
+ + The React name and logomark are properties of Facebook, + and their use herein is for learning and illustrative purposes only. + +
+ ); +} diff --git a/reactjs-todo-dv/client/components/layout/header.js b/reactjs-todo-dv/client/components/layout/header.js new file mode 100755 index 0000000..037817d --- /dev/null +++ b/reactjs-todo-dv/client/components/layout/header.js @@ -0,0 +1,165 @@ +/* + * forgerock-sample-web-react + * + * header.js + * + * Copyright (c) 2020 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useContext } from 'react'; +import { Link, useLocation, useSearchParams } from 'react-router-dom'; + +import AccountIcon from '../icons/account-icon'; +import { AppContext } from '../../global-state'; +import ForgeRockIcon from '../icons/forgerock-icon'; +import HomeIcon from '../icons/home-icon'; +import ReactIcon from '../icons/react-icon'; +import TodosIcon from '../icons/todos-icon'; + +/** + * @function Header - Header React view + * @returns {Object} - React component object + */ +export default function Header() { + /** + * Collects the global state for detecting user auth for rendering + * appropriate navigational items. + * The destructing of the hook's array results in index 0 having the state value, + * and index 1 having the "setter" method to set new state values. + */ + const [state] = useContext(AppContext); + const location = useLocation(); + const [params] = useSearchParams(); + + const centralLogin = params.get('centralLogin'); + const journey = params.get('journey'); + + const queryParams = {}; + + if (centralLogin) { + queryParams.centralLogin = centralLogin; + } + + if (journey) { + queryParams.journey = journey; + } + + const urlQueryParams = { + pathname: '/login', + search: new URLSearchParams(queryParams).toString(), + }; + + let TodosItem; + let LoginOrOutItem; + + /** + * Render different navigational items depending on authenticated status + */ + if (state.isAuthenticated) { + TodosItem = [ +
  • + + + Home + +
  • , +
  • + + + Todos + +
  • , + ]; + LoginOrOutItem = ( +
    +
    + +
      +
    • +
      +

      + {state.username} +

      +

      {state.email}

      +
      +
    • +
    • + + Sign Out + +
    • +
    +
    +
    + ); + } else { + TodosItem = null; + LoginOrOutItem = ( +
    + + Sign In + + + Sign Up + +
    + ); + } + + return ( + + ); +} diff --git a/reactjs-todo-dv/client/components/todo/todo.js b/reactjs-todo-dv/client/components/todo/todo.js new file mode 100644 index 0000000..5abc77f --- /dev/null +++ b/reactjs-todo-dv/client/components/todo/todo.js @@ -0,0 +1,98 @@ +/* + * forgerock-sample-web-react + * + * todo.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useContext, useEffect, useState } from 'react'; + +import { AppContext } from '../../global-state'; +import ActionIcon from '../icons/action-icon'; +import TodoIcon from '../icons/todo-icon'; + +/** + * @function Todo - Used for display a single todo and its details + * @param {Object} props - The object representing React's props + * @param {Object} props.setTodoActionId - Method from parent for passing the ID of todo + * @param {Object} props.todo - The todo object passed from the parent component + * @returns {Object} - React JSX view + */ +export default function Todo({ completeTodo, setSelectedDeleteTodo, setSelectedEditTodo, item }) { + const [state] = useContext(AppContext); + + /** + * The destructing of the hook's array results in index 0 having the state value, + * and index 1 having the "setter" method to set new state values. + */ + const [todo, setTodo] = useState(item); + const todoClasses = `cstm_todo-label ${ + todo.completed ? 'cstm_todo-label_complete' : 'cstm_todo-label_incomplete' + } ${'col d-flex align-items-center fs-5 w-100 p-3'}`; + + useEffect(() => { + setTodo(item); + }, [item]); + + return ( +
  • +
    +
    + { + completeTodo(todo._id, e.target.checked); + }} + /> + +
    + + +
    +
  • + ); +} diff --git a/reactjs-todo-dv/client/components/todos/create.js b/reactjs-todo-dv/client/components/todos/create.js new file mode 100644 index 0000000..cbc8035 --- /dev/null +++ b/reactjs-todo-dv/client/components/todos/create.js @@ -0,0 +1,72 @@ +/* + * forgerock-sample-web-react + * + * create-todo.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useContext, useRef, useState } from 'react'; + +import apiRequest from '../../utilities/request'; +import { AppContext } from '../../global-state'; + +/** + * @function CreateTodo - React component for displaying the input and button pair for todo creation + * @param {Object} props - React props object + * @param {Function} props.addTodo - The function that adds the todo to the local collection + * @returns {Object} - React component object + */ +export default function CreateTodo({ addTodo }) { + const [state] = useContext(AppContext); + + const [creatingTodo, setCreatingTodo] = useState(false); + const textInput = useRef(null); + + async function createTodo(e) { + e.preventDefault(); + + setCreatingTodo(true); + + const title = e.target.elements[0].value; + const newTodo = await apiRequest('todos', 'POST', { title }); + + addTodo(newTodo); + setCreatingTodo(false); + textInput.current.value = ''; + } + + return ( +
    +
    + + +
    + +
    + ); +} diff --git a/reactjs-todo-dv/client/components/todos/delete.js b/reactjs-todo-dv/client/components/todos/delete.js new file mode 100644 index 0000000..3428b0f --- /dev/null +++ b/reactjs-todo-dv/client/components/todos/delete.js @@ -0,0 +1,63 @@ +/* + * forgerock-sample-web-react + * + * delete.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useContext } from 'react'; + +import { AppContext } from '../../global-state'; + +/** + * @function Delete - Used for display a modal that ensures intention for todo deletion + * @param {Object} props - The object representing React's props + * @param {Object} props.deleteTodo - The todo object that is requested to be deleted + * @returns {Object} - React component object + */ +export default function Delete({ deleteTodo }) { + const [state] = useContext(AppContext); + + return ( + + ); +} diff --git a/reactjs-todo-dv/client/components/todos/edit.js b/reactjs-todo-dv/client/components/todos/edit.js new file mode 100644 index 0000000..79fdf35 --- /dev/null +++ b/reactjs-todo-dv/client/components/todos/edit.js @@ -0,0 +1,96 @@ +/* + * forgerock-sample-web-react + * + * edit.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useContext, useRef } from 'react'; + +import { AppContext } from '../../global-state'; + +/** + * @function Edit - Used for a single todo for edit within a modal popup + * @param {Object} props - The object representing React's props + * @param {Object} props.selectedEditTodo - The todo object representing what is selected for edit + * @param {Function} props.setSelectedEditTodo - The function to set the newly edited todo + * @param {Function} props.editTodo - The function to add the edited todo back to the collection + * @returns {Object} - React component object + */ +export default function Edit({ selectedEditTodo, setSelectedEditTodo, editTodo }) { + const [state] = useContext(AppContext); + const textInput = useRef(null); + + function updateTitle(e) { + setSelectedEditTodo({ ...selectedEditTodo, title: e.target.value }); + } + + function submit(e, type) { + e.preventDefault(); + + editTodo(selectedEditTodo); + + // TODO: Improve modal handling + if (type === 'form') { + document.getElementById('closeEditModalBtn').click(); + } + } + + return ( + + ); +} diff --git a/reactjs-todo-dv/client/components/todos/fetch.js b/reactjs-todo-dv/client/components/todos/fetch.js new file mode 100644 index 0000000..3547298 --- /dev/null +++ b/reactjs-todo-dv/client/components/todos/fetch.js @@ -0,0 +1,48 @@ +/* + * forgerock-sample-web-react + * + * fetch.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import apiRequest from '../../utilities/request'; + +/** + * @function useTodoFetch - A custom React hook for fetching todos from API + * @param {Function} dispatch - The function to pass in an action with data to result in new state + * @param {Function} setFetched - A function for setting the state of hasFetched + * @param {string} todosLength - The todo collection + * @returns {undefined} - this doesn't directly return anything, but calls dispatch to set data + */ +export default function useTodoFetch(dispatch, setFetched) { + const navigate = useNavigate(); + + /** + * Since we are making an API call, which is a side-effect, + * we will wrap this in a useEffect, which will re-render the + * view once the API request returns. + */ + useEffect(() => { + async function getTodos() { + // Request the todos from our resource API + const fetchedTodos = await apiRequest('todos', 'GET'); + + // TODO: improve error handling + if (fetchedTodos.error) { + return navigate('/login'); + } + setFetched(true); + dispatch({ type: 'init-todos', payload: { todos: fetchedTodos } }); + } + + getTodos(); + + // There are no dependencies needed as all methods/functions are "stable" + }, []); +} diff --git a/reactjs-todo-dv/client/components/todos/reducer.js b/reactjs-todo-dv/client/components/todos/reducer.js new file mode 100644 index 0000000..d16ac84 --- /dev/null +++ b/reactjs-todo-dv/client/components/todos/reducer.js @@ -0,0 +1,52 @@ +/* + * forgerock-sample-web-react + * + * reducer.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * @function reducer - A simple reducer for managing the state of todos + * @param {Object} state - The state of todos before applying the action + * @param {Object} action - Action object + * @param {string} action.type - Action type that describes what to do + * @param {Object} action.payload - The new state to be applied + * @returns {Array} - the new array of update todos + */ +export default function reducer(state, action) { + switch (action.type) { + case 'init-todos': + return [...action.payload.todos]; + case 'add-todo': + return [action.payload.todo, ...state]; + case 'delete-todo': + return state.filter((todo) => todo._id !== action.payload._id); + case 'complete-todo': + return state.map((todo) => { + if (todo._id === action.payload._id) { + return { + ...todo, + completed: action.payload.completed, + }; + } else { + return todo; + } + }); + case 'edit-todo': + return state.map((todo) => { + if (todo._id === action.payload._id) { + return { + ...todo, + title: action.payload.title, + }; + } else { + return todo; + } + }); + default: + throw new Error('Form action type not recognized.'); + } +} diff --git a/reactjs-todo-dv/client/components/todos/todo.js b/reactjs-todo-dv/client/components/todos/todo.js new file mode 100755 index 0000000..2c70b38 --- /dev/null +++ b/reactjs-todo-dv/client/components/todos/todo.js @@ -0,0 +1,101 @@ +/* + * forgerock-sample-web-react + * + * todo.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useContext, useEffect, useState } from 'react'; + +import { AppContext } from '../../global-state'; +import ActionIcon from '../icons/action-icon'; +import TodoIcon from '../icons/todo-icon'; + +/** + * @function Todo - Used for display a single todo and its details + * @param {Object} props - The object representing React's props + * @param {Function} props.completeTodo - A function for toggling a todo's complete state + * @param {Function} props.setSelectedDeleteTodo - A function for setting the todo for deletion + * @param {Function} props.setSelectedEditTodo - A function for setting the todo for edit + * @param {Object} props.item - The todo item instance to be rendered + * @returns {Object} - React component object + */ +export default function Todo({ completeTodo, setSelectedDeleteTodo, setSelectedEditTodo, item }) { + const [state] = useContext(AppContext); + + /** + * The destructing of the hook's array results in index 0 having the state value, + * and index 1 having the "setter" method to set new state values. + */ + const [todo, setTodo] = useState(item); + const todoClasses = `cstm_todo-label ${ + todo.completed ? 'cstm_todo-label_complete' : 'cstm_todo-label_incomplete' + } ${'col d-flex align-items-center fs-5 w-100 p-3'}`; + + useEffect(() => { + setTodo(item); + }, [item]); + + return ( +
  • +
    + { + completeTodo(todo._id, e.target.checked); + }} + /> + +
    + + +
  • + ); +} diff --git a/reactjs-todo-dv/client/components/utilities/back-home.js b/reactjs-todo-dv/client/components/utilities/back-home.js new file mode 100644 index 0000000..872ff08 --- /dev/null +++ b/reactjs-todo-dv/client/components/utilities/back-home.js @@ -0,0 +1,32 @@ +/* + * forgerock-sample-web-react + * + * back-home.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useContext } from 'react'; +import { Link } from 'react-router-dom'; + +import { AppContext } from '../../global-state'; +import LeftArrowIcon from '../icons/left-arrow-icon'; + +export default function BackHome() { + const [state] = useContext(AppContext); + const bootstrapClasses = 'btn btn-sm text-bold text-decoration-none d-inline-block fs-6 my-2'; + + return ( + + + Home + + ); +} diff --git a/reactjs-todo-dv/client/components/utilities/loading.js b/reactjs-todo-dv/client/components/utilities/loading.js new file mode 100644 index 0000000..ed2ecf4 --- /dev/null +++ b/reactjs-todo-dv/client/components/utilities/loading.js @@ -0,0 +1,35 @@ +/* + * forgerock-sample-web-react + * + * loading.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useContext } from 'react'; + +import { AppContext } from '../../global-state'; + +/** + * @function Loading - Used to display a loading message + * @param {Object} props - The object representing React's props + * @param {string} props.message - The message string object passed from the parent component + * @returns {Object} - React component object + */ +export default function Loading({ classes, message }) { + const [state] = useContext(AppContext); + return ( +
    +

    + + + + + {message} + +

    +
    + ); +} diff --git a/reactjs-todo-dv/client/constants.js b/reactjs-todo-dv/client/constants.js new file mode 100755 index 0000000..22cc81f --- /dev/null +++ b/reactjs-todo-dv/client/constants.js @@ -0,0 +1,17 @@ +/* + * forgerock-sample-web-react + * + * constants.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +export const API_URL = process.env.API_URL; +// Yes, the debugger boolean is intentionally reversed +export const DEBUGGER = process.env.DEBUGGER_OFF === 'true'; +export const CLIENT_ID = process.env.CLIENT_ID; +export const REDIRECT_URI = process.env.REDIRECT_URI; +export const SCOPE = process.env.SCOPE; +export const BASE_URL = process.env.BASE_URL; diff --git a/reactjs-todo-dv/client/global-state.js b/reactjs-todo-dv/client/global-state.js new file mode 100755 index 0000000..473d8f6 --- /dev/null +++ b/reactjs-todo-dv/client/global-state.js @@ -0,0 +1,143 @@ +/* + * forgerock-sample-web-react + * + * state.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { FRUser } from '@forgerock/javascript-sdk'; +import React, { useState } from 'react'; + +import { DEBUGGER } from './constants'; + +/** + * @function useStateMgmt - The global state/store for managing user authentication and page + * @param {Object} props - The object representing React's props + * @param {Object} props.email - User's email + * @param {Object} props.isAuthenticated - Boolean value of user's auth status + * @param {Object} props.prefersDarkTheme - User theme setting + * @param {Object} props.username - User's username + * @returns {Array} - Global state values and state methods + */ +export function useGlobalStateMgmt({ + email, + isAuthenticated, + prefersDarkTheme, + username, + loginClient, +}) { + /** + * Create state properties for "global" state. + * Using internal names that differ from external to prevent shadowing. + * The destructing of the hook's array results in index 0 having the state value, + * and index 1 having the "setter" method to set new state values. + */ + const [authenticated, setAuthentication] = useState(isAuthenticated || false); + const [mail, setEmail] = useState(email || ''); + const [name, setUser] = useState(username || ''); + + let theme; + + /** + * @function setAuthenticationWrapper - A wrapper for storing authentication in sessionStorage + * @param {boolean} value - current user authentication + * @returns {void} + */ + async function setAuthenticationWrapper(value) { + if (value === false) { + /** ********************************************************************* + * SDK INTEGRATION POINT + * Summary: Logout, end session and revoke tokens + * ---------------------------------------------------------------------- + * Details: Since this method is a global method via the Context API, + * any part of the application can log a user out. This is helpful when + * APIs are called and we get a 401 response. + ********************************************************************* */ + if (DEBUGGER) debugger; + try { + await FRUser.logout(); + } catch (err) { + console.error(`Error: logout did not successfully complete; ${err}`); + } + } + setAuthentication(value); + } + + /** + * @function setEmailWrapper - A wrapper for storing authentication in sessionStorage + * @param {string} value - current user's email + * @returns {void} + */ + function setEmailWrapper(value) { + window.sessionStorage.setItem('sdk_email', `${value}`); + setEmail(value); + } + + /** + * @function setUserWrapper - A wrapper for storing authentication in sessionStorage + * @param {string} value - current user's username + * @returns {void} + */ + function setUserWrapper(value) { + window.sessionStorage.setItem('sdk_username', `${value}`); + setUser(value); + } + + if (prefersDarkTheme) { + theme = { + mode: 'dark', + // CSS Classes + bgClass: 'bg-dark', + borderClass: 'border-dark', + borderHighContrastClass: 'cstm_border_black', + cardBgClass: 'cstm_card-dark', + dropdownClass: 'dropdown-menu-dark', + listGroupClass: 'cstm_list-group_dark', + navbarClass: 'cstm_navbar-dark navbar-dark bg-dark text-white', + textClass: 'text-white', + textMutedClass: 'text-white-50', + }; + } else { + theme = { + mode: 'light', + // CSS Classes + bgClass: '', + borderClass: '', + borderHighContrastClass: '', + cardBgClass: '', + dropdownClass: '', + listGroupClass: '', + navbarClass: 'navbar-light bg-white', + textClass: '', + textMutedClass: 'text-muted', + }; + } + + /** + * returns an array with state object as index zero and setters as index one + */ + return [ + { + isAuthenticated: authenticated, + email: mail, + theme, + username: name, + client: loginClient, + }, + { + setAuthentication: setAuthenticationWrapper, + setEmail: setEmailWrapper, + setUser: setUserWrapper, + }, + ]; +} + +/** + * @constant AppContext - Creates React Context API + * This provides the capability to set a global state in React + * without having to pass the state as props through parent-child components. + */ +export const AppContext = React.createContext([{}, {}]); diff --git a/reactjs-todo-dv/client/index.js b/reactjs-todo-dv/client/index.js new file mode 100755 index 0000000..28a5302 --- /dev/null +++ b/reactjs-todo-dv/client/index.js @@ -0,0 +1,118 @@ +/* + * forgerock-sample-web-react + * + * index.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { Config, TokenStorage } from '@forgerock/javascript-sdk'; +import davinciClient from '@forgerock/davinci-client'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import Router from './router'; +import { DEBUGGER, CLIENT_ID, REDIRECT_URI, SCOPE, BASE_URL } from './constants'; +import { AppContext, useGlobalStateMgmt } from './global-state'; + +/** + * This import will produce a separate CSS file linked in the index.html + * Webpack will detect this and transpile, process and generate the needed CSS file + */ +import './styles/index.scss'; + +/** *************************************************************************** + * SDK INTEGRATION POINT + * Summary: Configure the SDK + * ---------------------------------------------------------------------------- + * Details: Below, you will see the following settings: + * - clientId: (OAuth 2.0 only) this is the OAuth 2.0 client you created in PingOne + * - redirectUri: (OAuth 2.0 only) this is the URI/URL of this app to which the + * OAuth 2.0 flow redirects + * - scope: (OAuth 2.0 only) these are the OAuth scopes that you will request from + * PingOne + * - serverConfig: this includes the baseUrl of your PingOne environment + *************************************************************************** */ +if (DEBUGGER) debugger; + +const config = { + clientId: CLIENT_ID, + redirectUri: REDIRECT_URI, + scope: SCOPE, + serverConfig: { + baseUrl: BASE_URL, + wellknown: `${BASE_URL}as/.well-known/openid-configuration`, + }, +}; + +/** + * Initialize the React application + */ +(async function initAndHydrate() { + /** ************************************************************************* + * SDK INTEGRATION POINT + * Summary: Get OAuth/OIDC tokens from storage + * -------------------------------------------------------------------------- + * Details: We can immediately call TokenStorage.get() to check for stored + * tokens. If we have them, you can cautiously assume the user is + * authenticated. + ************************************************************************* */ + + // Create a DaVinci client for the login/reg flow + const loginClient = await davinciClient({ config }); + + await Config.setAsync(config); + + let isAuthenticated; + try { + isAuthenticated = !!(await TokenStorage.get()); + } catch (err) { + console.error(`Error: token retrieval for hydration; ${err}`); + } + + /** + * Pull custom values from outside of the app to (re)hydrate state. + */ + const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; + const email = window.sessionStorage.getItem('sdk_email'); + const username = window.sessionStorage.getItem('sdk_username'); + const rootEl = document.getElementById('root'); + + if (prefersDarkTheme) { + document.body.classList.add('cstm_bg-dark', 'bg-dark'); + } + + /** + * @function Init - Initializes React and global state + * @returns {Object} - React component object + */ + function Init() { + /** + * This leverages "global state" with React's Context API. + * This can be useful to share state with any component without + * having to pass props through deeply nested components, + * authentication status and theme state are good examples. + * + * If global state becomes a more complex function of the app, + * something like Redux might be a better option. + */ + const stateMgmt = useGlobalStateMgmt({ + email, + isAuthenticated, + prefersDarkTheme, + username, + loginClient, + }); + + return ( + + + + ); + } + + const root = ReactDOM.createRoot(rootEl); + // Mounts the React app to the existing root element + root.render(); +})(); diff --git a/reactjs-todo-dv/client/router.js b/reactjs-todo-dv/client/router.js new file mode 100755 index 0000000..9cb2ebb --- /dev/null +++ b/reactjs-todo-dv/client/router.js @@ -0,0 +1,66 @@ +/* + * forgerock-sample-web-react + * + * router.js + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import React, { useEffect } from 'react'; +import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom'; + +import { ProtectedRoute } from './utilities/route'; +import Todos from './views/todos'; +import Footer from './components/layout/footer'; +import Header from './components/layout/header'; +import Home from './views/home'; +import Login from './views/login'; +import Logout from './views/logout'; + +function ScrollToTop() { + const { pathname } = useLocation(); + + useEffect(() => { + window.scrollTo(0, 0); + }, [pathname]); + + return null; +} + +/** + * @function App - Application React view + * @returns {Object} - React component object + */ +export default function Router() { + return ( + + + } /> + +
    + +