├── .gitignore ├── LICENSE ├── README.md ├── email-templates ├── emailverification.html └── forgotPasswordEmail.html ├── frontend ├── .gitlab-ci.yml ├── Jenkinsfile ├── README.md ├── cicd │ └── test.sh ├── package.json ├── public │ ├── assets │ │ ├── images │ │ │ ├── brand │ │ │ │ ├── apple-touch-icon-76x76.png │ │ │ │ └── favicon-32x32.png │ │ │ └── flags │ │ │ │ ├── en.png │ │ │ │ └── es.png │ │ └── img │ │ │ └── brand │ │ │ ├── apple-touch-icon-76x76.png │ │ │ └── favicon-32x32.png │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── assets │ ├── css │ │ └── custom.css │ └── img │ │ └── brand │ │ └── appIcon.png │ ├── components │ ├── App │ │ ├── App.jsx │ │ ├── App.test.jsx │ │ └── index.jsx │ ├── CustomButton │ │ ├── CustomButton.jsx │ │ └── index.jsx │ ├── Form │ │ └── Input │ │ │ ├── Input.jsx │ │ │ ├── InputAddon.jsx │ │ │ ├── InputCheckbox.jsx │ │ │ ├── InputError.jsx │ │ │ ├── InputLabel.jsx │ │ │ ├── InputText.jsx │ │ │ └── index.jsx │ ├── NavDropdown │ │ ├── GuestNavDropdown │ │ │ ├── GuestLinks.json │ │ │ ├── GuestNavDropdown.jsx │ │ │ └── index.jsx │ │ ├── LanguageDropdown │ │ │ ├── LanguageDropdown.jsx │ │ │ └── index.jsx │ │ └── MemberNavDropdown │ │ │ ├── MemberLinks.json │ │ │ ├── MemberNavDropdown.jsx │ │ │ └── index.jsx │ ├── Navbar │ │ ├── Navbar.jsx │ │ └── index.jsx │ └── Util │ │ ├── Authenticating │ │ ├── Authenticating.jsx │ │ └── index.jsx │ │ ├── Loading │ │ ├── Loading.jsx │ │ └── index.jsx │ │ ├── NotAuthorized │ │ ├── NotAuthorized.jsx │ │ └── index.jsx │ │ └── PageData │ │ ├── PageData.jsx │ │ └── index.jsx │ ├── config │ ├── config.jsx │ ├── index.jsx │ └── links.jsx │ ├── data │ └── language │ │ ├── en │ │ └── common.json │ │ ├── es │ │ └── common.json │ │ └── languages.json │ ├── index.js │ ├── layouts │ ├── DashLayout.jsx │ └── index.jsx │ ├── redux │ ├── actionTypes │ │ ├── actionTypes.jsx │ │ └── index.jsx │ ├── actions │ │ ├── index.jsx │ │ ├── language.jsx │ │ └── user.jsx │ ├── reducers │ │ ├── index.jsx │ │ ├── languageReducer.jsx │ │ ├── rootReducer.jsx │ │ └── userReducer.jsx │ └── store │ │ ├── index.jsx │ │ └── store.jsx │ ├── routes.js │ ├── serviceWorker.js │ ├── util │ ├── APIFetch │ │ ├── APIFetch.jsx │ │ └── index.jsx │ ├── Auth │ │ ├── Auth.jsx │ │ ├── AuthAPI.jsx │ │ └── index.jsx │ ├── FormHandler │ │ ├── FormHandler.jsx │ │ └── index.jsx │ ├── History │ │ ├── History.jsx │ │ └── index.jsx │ ├── Language │ │ ├── Language.jsx │ │ └── index.jsx │ ├── Logout │ │ ├── Logout.jsx │ │ ├── LogoutAPI.jsx │ │ └── index.jsx │ ├── PrivateRoute │ │ ├── PrivateRoute.jsx │ │ └── index.jsx │ ├── ResponseHandler │ │ ├── ResponseHandler.jsx │ │ └── index.jsx │ ├── Toasty │ │ ├── Toasty.jsx │ │ └── index.jsx │ └── ValidateInput │ │ ├── ValidateInput.jsx │ │ └── index.jsx │ └── views │ ├── Auth │ ├── ChangePassword │ │ ├── ChangePassword.jsx │ │ ├── ChangePasswordAPI.jsx │ │ ├── ChangePasswordController.jsx │ │ ├── ChangePasswordForm.json │ │ └── index.jsx │ ├── ForgotPassword │ │ ├── ForgotPassword.jsx │ │ ├── ForgotPasswordAPI.jsx │ │ ├── ForgotPasswordController.jsx │ │ ├── ForgotPasswordForm.json │ │ └── index.jsx │ ├── Login │ │ ├── Login.jsx │ │ ├── LoginAPI.jsx │ │ ├── LoginController.jsx │ │ ├── LoginForm.json │ │ └── index.jsx │ ├── Register │ │ ├── Register.jsx │ │ ├── RegisterAPI.jsx │ │ ├── RegisterForm.json │ │ └── index.jsx │ └── Verify │ │ ├── Verify2Factor │ │ ├── Verify2Factor.jsx │ │ ├── Verify2FactorAPI.jsx │ │ ├── Verify2FactorController.jsx │ │ ├── Verify2FactorForm.json │ │ └── index.jsx │ │ ├── VerifyEmail │ │ ├── VerifyEmail.jsx │ │ ├── VerifyEmailAPI.jsx │ │ ├── VerifyEmailForm.json │ │ ├── VerifyEmailPageData.jsx │ │ └── index.jsx │ │ └── index.jsx │ ├── Home.jsx │ ├── NotFound │ ├── NotFound.jsx │ └── index.jsx │ ├── Todo │ ├── TodoAPI.jsx │ ├── TodoList │ │ ├── TodoList.jsx │ │ ├── TodoListPageData.jsx │ │ └── index.jsx │ ├── TodoModify │ │ ├── TodoModify.jsx │ │ ├── TodoModifyForm.json │ │ ├── TodoModifyPageData.jsx │ │ └── index.jsx │ ├── TodoView │ │ ├── TodoView.jsx │ │ ├── TodoViewPageData.jsx │ │ └── index.jsx │ └── index.jsx │ └── User │ ├── User2Factor │ ├── Disable │ │ ├── Disable.jsx │ │ ├── Disable2FAAPI.jsx │ │ ├── Disable2FAController.jsx │ │ ├── Disable2FAForm.json │ │ └── index.jsx │ ├── Enable │ │ ├── Enable.jsx │ │ ├── Enable2FAAPI.jsx │ │ ├── Enable2FAController.jsx │ │ ├── Enable2FAForm.json │ │ └── index.jsx │ ├── User2Factor.jsx │ ├── User2FactorAPI.jsx │ └── index.jsx │ ├── UserChangePassword │ ├── UserChangePassword.jsx │ ├── UserChangePasswordAPI.jsx │ ├── UserChangePasswordController.jsx │ ├── UserChangePasswordForm.json │ └── index.jsx │ ├── UserProfile │ ├── UserProfileEdit │ │ ├── UserProfileEdit.jsx │ │ ├── UserProfileEditAPI.jsx │ │ ├── UserProfileEditController.jsx │ │ ├── UserProfileEditForm.json │ │ ├── UserProfileEditPageData.jsx │ │ └── index.jsx │ ├── UserProfileView │ │ ├── UserProfileView.jsx │ │ ├── UserProfileViewAPI.jsx │ │ ├── UserProfileViewPageData.jsx │ │ └── index.jsx │ └── index.jsx │ └── index.jsx ├── mongodb └── roles.json └── server ├── .dockerignore ├── .drone.yaml ├── .example.env ├── .gitlab-ci.yml ├── Dockerfile ├── Jenkinsfile ├── README.md ├── package-lock.json ├── package.json ├── server.js └── src ├── components ├── fusionAuth │ ├── fusionAuth.js │ ├── fusionAuthAPI.js │ ├── fusionAuthInputs.json │ └── index.js ├── health │ ├── health.js │ └── index.js ├── roles │ ├── index.js │ ├── roles.js │ └── rolesModel.js ├── todo │ ├── index.js │ ├── todo.js │ ├── todoInputs.json │ └── todoModel.js └── user │ ├── index.js │ ├── user.js │ ├── userAPI.js │ └── userInputs.json ├── config ├── config.js └── index.js ├── data ├── fusionAuth │ ├── fusionAuthData.js │ └── index.js └── language │ ├── en │ ├── common.json │ └── index.js │ └── es │ ├── common.json │ └── index.js ├── database ├── database.js └── index.js ├── expressServer ├── expressAPI.js ├── expressMiddleware.js ├── expressRoutes.js ├── expressServer.js └── index.js └── util ├── apiFetch ├── apiFetch.js └── index.js ├── auth ├── auth.js └── index.js ├── language ├── index.js └── language.js └── validForm ├── index.js └── validForm.js /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/build 3 | **/npm-debug.log 4 | *.DS_Store 5 | server/public 6 | **/package-lock.json 7 | .env 8 | **/.gitignore -------------------------------------------------------------------------------- /frontend/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:latest 2 | 3 | stages: 4 | - install 5 | - test 6 | - build 7 | - deploy 8 | 9 | variables: 10 | AWS_DEFAULT_REGION: us-east-2 11 | BUCKET_NAME: fusionauth-demo 12 | ENV_PROD: https://fusionauth-demo.domain.com 13 | CLOUDFRONT_ID: some-id 14 | 15 | install: 16 | stage: install 17 | script: 18 | - npm ci --only=production 19 | artifacts: 20 | paths: 21 | - node_modules/ 22 | cache: 23 | key: node 24 | paths: 25 | - node_modules/ 26 | 27 | test: 28 | stage: test 29 | dependencies: 30 | - install 31 | script: 32 | - chmod +x ./cicd/test.sh 33 | - ./cicd/test.sh 34 | 35 | build: 36 | stage: build 37 | dependencies: 38 | - install 39 | script: 40 | - npm run build 41 | artifacts: 42 | paths: 43 | - build/ 44 | cache: 45 | key: build 46 | paths: 47 | - build/ 48 | 49 | deploy_prod: 50 | image: "python:latest" 51 | stage: deploy 52 | dependencies: 53 | - build 54 | before_script: 55 | - pip install awscli 56 | script: 57 | - aws s3 sync --cache-control 'max-age=604800' --delete dist/ s3://${BUCKET_NAME} 58 | - aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_ID} --paths "/*" 59 | environment: 60 | name: Production 61 | url: ${ENV_PROD} 62 | when: manual 63 | only: 64 | - master -------------------------------------------------------------------------------- /frontend/Jenkinsfile: -------------------------------------------------------------------------------- 1 | podTemplate(label: "mypod", containers: [ 2 | containerTemplate(name: "node", image: "node", ttyEnabled: true, command: "cat"), 3 | ]) { 4 | node("mypod") { 5 | def uatBucket = "fusionauth-demo" 6 | def prodBucket = "fusionauth-demo" 7 | 8 | stage ("Checkout Code") { 9 | checkout scm 10 | } 11 | 12 | container("node") { 13 | stage("Install Packages") { 14 | sh "npm install" 15 | } 16 | 17 | stage("Test and Build") { 18 | parallel( 19 | "Run Tests": { 20 | sh "chmod +x ./cicd/test.sh" 21 | sh "./cicd/test.sh" 22 | }, 23 | "Create Build Artifacts": { 24 | sh "npm run build" 25 | } 26 | ) 27 | } 28 | 29 | if (env.BRANCH_NAME == "master") { 30 | stage("Deploy - Production") { 31 | withAWS(region:"us-east-2", credentials:"credentials-id") { 32 | s3Delete(bucket: "${prodBucket}", path:"/") 33 | s3Upload(bucket: "${prodBucket}", workingDir:"build", includePathPattern:"**/*", cacheControl:"public,max-age=604800", excludePathPattern:"**/index.html"); 34 | s3Upload(bucket: "${prodBucket}", workingDir:"build", includePathPattern:"**/index.html", cacheControl:"no-cache"); 35 | } 36 | } 37 | } else { 38 | stage("Deploy - Staging") { 39 | withAWS(region:"us-east-2", credentials:"credentials-id") { 40 | s3Delete(bucket: "${uatBucket}", path:"/") 41 | s3Upload(bucket: "${uatBucket}", workingDir:"build", includePathPattern:"**/*", cacheControl:"public,max-age=604800", excludePathPattern:"**/index.html"); 42 | s3Upload(bucket: "${uatBucket}", workingDir:"build", includePathPattern:"**/index.html", cacheControl:"no-cache"); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # FusionAuth React Example 2 | 3 | This is the React portion of the FusionAuth NodeJS + React Todo Example. 4 | 5 | ## About 6 | The React application uses React Redux for state storage for information about the user and language data, though there are more optimal solutions for larger applications for language state. Font awesome is used for icons in the application and axios is used for making requests to FusionAuth or the API server. All inputs are validated through a custom validation module prior to submitting any API request. 7 | 8 | ## Prerequisites 9 | * Have already installed dependencies with `npm install`. 10 | 11 | ## Configuration 12 | 13 | Configuration is done in `src/config/config.jsx`. Expressed in dot notation below. 14 | 15 | | Parameter | Description | Example | 16 | | ------------- | ------------- | ----- | 17 | | **FusionAuth** | 18 | | fusionAuth.BASEURL | The URL where FusionAuth is hosted and set up. | `http://localhost:9011` | 19 | | fusionAuth.APPLICATION_ID | The generated application ID for the app created in the FusionAuth client for the demo. | `10e4d908-7655-44af-abf0-1a031aff519a` | 20 | | **API Server** | 21 | | apiServer.BASEURL | The URL where the API server is hosted and set up. | `http://localhost:5000` | 22 | | **React** | 23 | | app.TWO_FA_NAME | A name to be given to React Redux for state storage in the browser. | `fusionAuthDemoApp` | -------------------------------------------------------------------------------- /frontend/cicd/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo "The following "npm" command tests that your simple Node.js/React" 4 | echo "application renders satisfactorily. This command actually invokes the test" 5 | echo "runner Jest (https://facebook.github.io/jest/)." 6 | set -x 7 | npm test -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fusionauth-nodejs-react-example", 3 | "description": "An example application using NodeJS, ExpressJS, ReactJS and FusionAuth", 4 | "version": "1.0.0", 5 | "author": "FusionAuth", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "scripts": { 9 | "start": "react-scripts start", 10 | "build": "react-scripts build", 11 | "test": "cross-env CI=true react-scripts test", 12 | "eject": "react-scripts eject" 13 | }, 14 | "contributors": [ 15 | { 16 | "name": "Tyler Engelhardt", 17 | "url": "https://mainelysoftware.com", 18 | "email": "tyler@mainelysoftware.com" 19 | } 20 | ], 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/FusionAuth/fusionauth-nodejs-react-example/issues" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/FusionAuth/fusionauth-nodejs-react-example" 30 | }, 31 | "dependencies": { 32 | "@fortawesome/fontawesome-svg-core": "^1.2.22", 33 | "@fortawesome/free-solid-svg-icons": "^5.10.2", 34 | "@fortawesome/react-fontawesome": "^0.1.4", 35 | "axios": "^0.19.0", 36 | "bootstrap": "^4.3.1", 37 | "cross-env": "^5.2.1", 38 | "lodash": "^4.17.15", 39 | "qrcode.react": "^0.9.3", 40 | "react": "^16.9.0", 41 | "react-dom": "^16.9.0", 42 | "react-redux": "^7.1.1", 43 | "react-router-dom": "^5.0.1", 44 | "react-scripts": "3.0.1", 45 | "react-telephone-input": "^4.73.4", 46 | "react-toastify": "^5.3.2", 47 | "reactstrap": "^8.0.0", 48 | "redux": "^4.0.4", 49 | "redux-persist": "^6.0.0", 50 | "redux-thunk": "^2.3.0" 51 | }, 52 | "browserslist": { 53 | "production": [ 54 | ">0.2%", 55 | "not dead", 56 | "not op_mini all" 57 | ], 58 | "development": [ 59 | "last 1 chrome version", 60 | "last 1 firefox version", 61 | "last 1 safari version" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /frontend/public/assets/images/brand/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionAuth/fusionauth-nodejs-react-example/b0a2ba366c1d7c4fbcd507a296a0845d072221a6/frontend/public/assets/images/brand/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /frontend/public/assets/images/brand/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionAuth/fusionauth-nodejs-react-example/b0a2ba366c1d7c4fbcd507a296a0845d072221a6/frontend/public/assets/images/brand/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/assets/images/flags/en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionAuth/fusionauth-nodejs-react-example/b0a2ba366c1d7c4fbcd507a296a0845d072221a6/frontend/public/assets/images/flags/en.png -------------------------------------------------------------------------------- /frontend/public/assets/images/flags/es.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionAuth/fusionauth-nodejs-react-example/b0a2ba366c1d7c4fbcd507a296a0845d072221a6/frontend/public/assets/images/flags/es.png -------------------------------------------------------------------------------- /frontend/public/assets/img/brand/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionAuth/fusionauth-nodejs-react-example/b0a2ba366c1d7c4fbcd507a296a0845d072221a6/frontend/public/assets/img/brand/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /frontend/public/assets/img/brand/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionAuth/fusionauth-nodejs-react-example/b0a2ba366c1d7c4fbcd507a296a0845d072221a6/frontend/public/assets/img/brand/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionAuth/fusionauth-nodejs-react-example/b0a2ba366c1d7c4fbcd507a296a0845d072221a6/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 16 | 17 | FusionAuth - NodeJS & ReactJS Example 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "FusionAuth Example", 3 | "name": "FusionAuth - NodeJS & ReactJS Example", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /frontend/src/assets/img/brand/appIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionAuth/fusionauth-nodejs-react-example/b0a2ba366c1d7c4fbcd507a296a0845d072221a6/frontend/src/assets/img/brand/appIcon.png -------------------------------------------------------------------------------- /frontend/src/components/App/App.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { Router, Route, Switch } from "react-router-dom"; 4 | import { ToastContainer } from "react-toastify"; 5 | import { Provider } from "react-redux"; 6 | import { PersistGate } from "redux-persist/integration/react"; 7 | import { library } from "@fortawesome/fontawesome-svg-core"; 8 | import { 9 | faTimes, 10 | faKey, 11 | faPowerOff, 12 | faUser, 13 | faUserSecret, 14 | faUserNinja, 15 | faUserTie, 16 | faUserPlus, 17 | faEnvelope, 18 | faUserLock, 19 | faUserShield, 20 | faLock, 21 | faSync, 22 | faPhone, 23 | faClipboardList, 24 | faCheck, 25 | faTasks, 26 | faFeatherAlt, 27 | faCode, 28 | faShieldAlt 29 | } from "@fortawesome/free-solid-svg-icons"; 30 | 31 | // Styling 32 | import "react-toastify/dist/ReactToastify.min.css"; 33 | import "bootstrap/dist/css/bootstrap.min.css"; 34 | import "../../assets/css/custom.css"; 35 | 36 | // History 37 | import History from "../../util/History"; 38 | 39 | // Layouts 40 | import DashLayout from "../../layouts/DashLayout"; 41 | 42 | // Redux 43 | import { store, persistor } from "../../redux/store"; 44 | 45 | // Font Awesome Initialization 46 | library.add( 47 | faTimes, 48 | faKey, 49 | faPowerOff, 50 | faUser, 51 | faUserSecret, 52 | faUserNinja, 53 | faUserTie, 54 | faUserPlus, 55 | faEnvelope, 56 | faUserLock, 57 | faUserShield, 58 | faLock, 59 | faSync, 60 | faPhone, 61 | faClipboardList, 62 | faCheck, 63 | faTasks, 64 | faFeatherAlt, 65 | faCode, 66 | faShieldAlt 67 | ); 68 | 69 | /** 70 | * Application Component 71 | * 72 | * This is where we declare the application. We use BrowserRouter to encapsulate 73 | * the application for routing purposes, and then we use Switch to be able to 74 | * handle the different routes. 75 | */ 76 | const App = () => { 77 | return ( 78 | 79 | 80 | 81 | 82 | 83 | } /> 84 | 85 | 86 | 87 | 88 | ); 89 | }; 90 | 91 | // Export the Application Component. 92 | export default App; -------------------------------------------------------------------------------- /frontend/src/components/App/App.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); -------------------------------------------------------------------------------- /frontend/src/components/App/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import App from "./App"; 3 | 4 | // Export modules 5 | export default App; -------------------------------------------------------------------------------- /frontend/src/components/CustomButton/CustomButton.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { Button } from "reactstrap"; 4 | 5 | /** 6 | * Custom Button Component 7 | * 8 | * @param {String} color Button color 9 | * @param {String} text Button text 10 | * @param {String} type Button type 11 | * @param {Boolean} disabled Button disabled status 12 | * @param {String} className Button classes 13 | * @param {Function} onClick onClick function handler 14 | * 15 | */ 16 | const CustomButton = ({ color, text, type, disabled, className, onClick }) => ( 17 | 20 | ); 21 | 22 | // Export the Custom Button component. 23 | export default CustomButton; -------------------------------------------------------------------------------- /frontend/src/components/CustomButton/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import CustomButton from "./CustomButton"; 3 | 4 | // Export modules 5 | export default CustomButton; -------------------------------------------------------------------------------- /frontend/src/components/Form/Input/Input.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Input Component 3 | * 4 | * References the different types of inputs available in order to display 5 | * them properly. 6 | */ 7 | 8 | // Dependencies 9 | import React from "react"; 10 | 11 | // Components 12 | import InputCheckbox from "./InputCheckbox"; 13 | //import InputFile from "./InputFile"; 14 | import InputText from "./InputText"; 15 | 16 | // Available types of inputs 17 | const availableInputs = { 18 | checkbox: InputCheckbox, 19 | //file: InputFile, 20 | text: InputText 21 | }; 22 | 23 | /** 24 | * Input Component 25 | * 26 | * Custom input component that displays the proper custom elements if they 27 | * exist and are defined above. 28 | * 29 | * @param {Object} props Properties from the parent function. 30 | */ 31 | const Input = props => { 32 | // Get the right value for the input based on its type. 33 | const inputValue = props.type === "checkbox" || props.type === "toggle" 34 | ? props.value || false 35 | : props.value || ""; 36 | 37 | // Determine if the Custom input should be displayed, or the InputDNE. 38 | const Input = availableInputs[props.type] ? availableInputs[props.type] : availableInputs["text"]; 39 | 40 | // Return the Custom Input. 41 | return ; 42 | } 43 | 44 | // Export the Custom Input component. 45 | export default Input; -------------------------------------------------------------------------------- /frontend/src/components/Form/Input/InputAddon.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { isEmpty } from "lodash"; 4 | import { InputGroupAddon, InputGroupText } from "reactstrap"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | 7 | /** 8 | * Custom Input Addon 9 | * 10 | * Displays a custom input addon for form inputs. Can be used for prepend or append 11 | * addons. 12 | * 13 | * @param {Object} type Type of addon (prepend || append). 14 | * @param {Object} icon Icon to be displayed. 15 | */ 16 | const InputAddon = ({ type, icon }) => { 17 | return !isEmpty(type) && !isEmpty(icon) 18 | ? 19 | 20 | 21 | 22 | 23 | : ""; 24 | } 25 | 26 | // Export the Custom Input Addon component. 27 | export default InputAddon; 28 | -------------------------------------------------------------------------------- /frontend/src/components/Form/Input/InputCheckbox.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { isFunction } from "lodash"; 4 | import { Col, FormGroup, FormFeedback, Input, FormText } from "reactstrap"; 5 | 6 | // Components 7 | import InputLabel from "./InputLabel"; 8 | import InputError from "./InputError"; 9 | 10 | // Utils 11 | import ValidateInput from "../../../util/ValidateInput"; 12 | 13 | /** 14 | * Input Checkbox Component 15 | * 16 | * Custom input checkbox component that displays the input and related components for 17 | * the input depending on the information supplied. 18 | * 19 | * @param {Object} props Properties from the parent function. 20 | */ 21 | const InputCheckbox = props => { 22 | // Properties for the input display. 23 | const { 24 | inputColXL, 25 | inputColMD, 26 | inputColClassName, 27 | id, 28 | autoFocus, 29 | disabled, 30 | formGroupClassName, 31 | inputClassName, 32 | inputCheckboxClassName, 33 | inputStyle, 34 | label, 35 | labelClassName, 36 | labelMuted, 37 | name, 38 | validate, 39 | onFocus, 40 | onChange, 41 | tabIndex, 42 | value, 43 | success, 44 | error, 45 | formHelpText, 46 | } = props; 47 | 48 | /** 49 | * Handle input validation 50 | * 51 | * Validate inputs on blur. 52 | * 53 | * @param {Object} target The input that fired the event. 54 | */ 55 | const handleValidate = ({ target }) => { 56 | validate(target.name, ValidateInput(value, target.type, props.validation, props.languageData)); 57 | }; 58 | 59 | // Display the input and related content. 60 | return ( 61 | 62 | 63 | 77 | 78 | { success } 79 | 80 | { formHelpText } 81 | 82 | 83 | ); 84 | } 85 | 86 | // Export the Custom Input Checkbox component. 87 | export default InputCheckbox; 88 | -------------------------------------------------------------------------------- /frontend/src/components/Form/Input/InputError.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React, { useState, useEffect } from "react"; 3 | import { isEmpty } from "lodash"; 4 | import { FormFeedback} from "reactstrap"; 5 | 6 | /** 7 | * Input Error Component 8 | * 9 | * Custom error display for inputs that converts the array of errors into 10 | * a single set of list items. 11 | * 12 | * @param {Array} errors Array of errors for the input item. 13 | */ 14 | const InputError = ({ errors }) => { 15 | // Set and get input errors with React Hook. 16 | const [inputErrors, setInputErrors] = useState([]); 17 | 18 | // Use `useEffect` to set and update the errors as they change. 19 | useEffect(() => { 20 | /** 21 | * Update the errors for the input item. 22 | */ 23 | const updateErrors = () => { 24 | // If the error array is empty, set the error state to an empty string. 25 | if (isEmpty(errors)) { 26 | setInputErrors(""); 27 | } else { 28 | // Set an empty array for the error state. 29 | let listErrors = []; 30 | 31 | // Loop through each error in the passed array and push an `li` item to the error state. 32 | errors.forEach((e, key) => listErrors.push(
  • { e }
  • )); 33 | 34 | // Set the error state. 35 | setInputErrors(listErrors); 36 | } 37 | } 38 | 39 | // Call the error update function. 40 | updateErrors(); 41 | }, [errors]); 42 | 43 | // Display the error(s). 44 | return { inputErrors }; 45 | } 46 | 47 | // Export the Custom Input Error component. 48 | export default InputError; 49 | -------------------------------------------------------------------------------- /frontend/src/components/Form/Input/InputLabel.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { isEmpty } from "lodash"; 4 | import { Label } from "reactstrap"; 5 | 6 | /** 7 | * Custom Input Label 8 | * 9 | * Displays a custom input label for form inputs. 10 | * 11 | * @param {Object} props Properties from the parent function. 12 | */ 13 | const InputLabel = props => { 14 | return !isEmpty(props.htmlFor) && !isEmpty(props.label) 15 | ? 23 | : ""; 24 | } 25 | 26 | // Export the Custom Input Label component. 27 | export default InputLabel; 28 | -------------------------------------------------------------------------------- /frontend/src/components/Form/Input/InputText.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { isFunction } from "lodash"; 4 | import { Col, FormGroup, InputGroup, FormFeedback, Input, FormText } from "reactstrap"; 5 | 6 | // Components 7 | import InputLabel from "./InputLabel"; 8 | import InputAddon from "./InputAddon"; 9 | import InputError from "./InputError"; 10 | 11 | // Utils 12 | import ValidateInput from "../../../util/ValidateInput"; 13 | 14 | /** 15 | * Input Text Component 16 | * 17 | * Custom input text component that displays the input and related components for 18 | * the input depending on the information supplied. Text is used for all inputs, 19 | * excluding file and checkbox. 20 | * 21 | * @param {Object} props Properties from the parent function. 22 | */ 23 | const InputText = props => { 24 | // Properties for the input display. 25 | const { 26 | inputColXL, 27 | inputColMD, 28 | inputColClassName, 29 | id, 30 | type, 31 | autoFocus, 32 | disabled, 33 | inputClassName, 34 | label, 35 | labelClassName, 36 | labelMuted, 37 | name, 38 | value, 39 | rows, 40 | placeholder, 41 | autoComplete, 42 | validate, 43 | secondValue, 44 | onChange, 45 | handleFormSubmit, 46 | onFocus, 47 | tabIndex, 48 | prependIcon, 49 | appendIcon, 50 | success, 51 | error, 52 | formHelpText, 53 | pattern, 54 | maxLength 55 | } = props; 56 | 57 | /** 58 | * Handle input validation 59 | * 60 | * Validate inputs on blur. 61 | * 62 | * @param {Object} target The input that fired the event. 63 | */ 64 | const handleValidate = ({ target }) => { 65 | validate(target.name, ValidateInput(target.value, target.type, props.validation, props.languageData, secondValue)); 66 | }; 67 | 68 | /** 69 | * Handle Key Press 70 | * 71 | * Handles key presses for the input. Work around for some inputs that 72 | * try to refresh the page for an unknown reason. Instead of allowing the 73 | * page to be refreshed, it attempts to submit the form. 74 | * 75 | * @param {Object} e Input object that executed the function. 76 | */ 77 | const handleKeyPress = e => { 78 | e.key === "Enter" && isFunction(handleFormSubmit) && handleFormSubmit(e); 79 | }; 80 | 81 | // Display the input and related content. 82 | return ( 83 | 84 | 85 | 86 | 87 | 88 | { /* Temporary fix for preventing the form arbitrarily submitting when it shouldn't */ } 89 | { /* Handled by onKeyPress */ } 90 | 109 | 110 | { success } 111 | 112 | 113 | { formHelpText } 114 | 115 | 116 | ); 117 | } 118 | 119 | // Export the Custom Input Text component. 120 | export default InputText; 121 | -------------------------------------------------------------------------------- /frontend/src/components/Form/Input/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Input from "./Input"; 3 | 4 | // Export modules 5 | export default Input; -------------------------------------------------------------------------------- /frontend/src/components/NavDropdown/GuestNavDropdown/GuestLinks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "icon": "key", 4 | "text": [ 5 | "common", 6 | "login" 7 | ], 8 | "link": [ 9 | "auth", 10 | "login" 11 | ] 12 | }, 13 | { 14 | "icon": "user-plus", 15 | "text": [ 16 | "common", 17 | "register" 18 | ], 19 | "link": [ 20 | "auth", 21 | "register" 22 | ] 23 | } 24 | ] -------------------------------------------------------------------------------- /frontend/src/components/NavDropdown/GuestNavDropdown/GuestNavDropdown.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { Link } from "react-router-dom"; 4 | import { get, map } from "lodash"; 5 | import { connect } from "react-redux"; 6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 7 | import { 8 | DropdownMenu, 9 | DropdownItem 10 | } from "reactstrap"; 11 | 12 | // Application Links 13 | import { links } from "../../../config"; 14 | import guestLinks from "./GuestLinks"; 15 | 16 | /** 17 | * Guest Navbar Component 18 | * 19 | * Contains the display components and associated functions for the guest 20 | * dropdown for the main navbar. 21 | * 22 | * @param {Object} languageData Current language information for the app. Language data object. 23 | */ 24 | const GuestNavDropdown = ({ languageData }) => ( 25 | 26 | 27 |
    { get(languageData, ["common", "welcome"]) }
    28 |
    29 | { 30 | map(guestLinks, (link, key) => ( 31 | 32 |   33 | { get(languageData, link["text"]) } 34 | 35 | )) 36 | } 37 |
    38 | ); 39 | 40 | /** 41 | * Get App State 42 | * 43 | * Get the requried state for the component from the Redux store. 44 | * 45 | * @param {Object} state Application state from Redux. 46 | */ 47 | const mapStateToProps = state => { 48 | return { 49 | languageData: state.language.languageData 50 | } 51 | } 52 | 53 | // Export the Guest Navbar Component. 54 | export default connect(mapStateToProps)(GuestNavDropdown); -------------------------------------------------------------------------------- /frontend/src/components/NavDropdown/GuestNavDropdown/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import GuestNavDropdown from "./GuestNavDropdown"; 3 | 4 | // Export modules 5 | export default GuestNavDropdown; -------------------------------------------------------------------------------- /frontend/src/components/NavDropdown/LanguageDropdown/LanguageDropdown.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React, { useEffect, useRef } from "react"; 3 | import { get, map } from "lodash"; 4 | import { connect } from "react-redux"; 5 | import { 6 | UncontrolledDropdown, 7 | DropdownToggle, 8 | DropdownMenu, 9 | DropdownItem, 10 | Media 11 | } from "reactstrap"; 12 | 13 | // Toasty 14 | import Toasty from "../../../util/Toasty"; 15 | 16 | // Language 17 | import { setLanguage } from "../../../redux/actions"; 18 | import languages from "../../../data/language/languages.json"; 19 | 20 | /** 21 | * Language Dropdown Component 22 | * 23 | * Contains the display components and associated functions for the guest 24 | * dropdown for the main navbar. 25 | * 26 | * @param {Function} setLanguage Redux action to change the app's language. 27 | * @param {Object} languageData Current language information for the app. Language data object. 28 | */ 29 | const LanguageDropdown = ({ setLanguage, languageData }) => { 30 | // Setup an initial mount reference so we can send Toast notifications 31 | // on update of the languageData. 32 | const initialMount = useRef(true); 33 | 34 | /** 35 | * Listen for updates 36 | * 37 | * Send the user a notification that their language preference was saved. 38 | */ 39 | useEffect(() => { 40 | /** 41 | * Notify Function 42 | * 43 | * Notify the user. 44 | */ 45 | const doNotify = () => { 46 | // Make sure we don't send a notification if this is the inital render. 47 | if (initialMount.current) { 48 | initialMount.current = false; 49 | } else { 50 | // Display a toast notification that the language was empty. 51 | Toasty.notify({ 52 | type: Toasty.info(), 53 | content: get(languageData, ["common", "changeLang", "saved"]) 54 | }); 55 | } 56 | }; 57 | 58 | 59 | // Call the notificaiton function. 60 | doNotify(); 61 | }, [languageData]); 62 | 63 | // Display the Language Dropdown. 64 | return ( 65 | 66 | 67 | 68 | 69 | { 73 | 74 | 75 | 76 | { get(languageData, ["common", "langData", "name"]) } 77 | 78 | 79 | 80 | 81 | 82 | { 83 | map(languages, (lang, key) => ( 84 | 85 | 86 | { 91 | 92 |  { get(lang, "name") } 93 | 94 | )) 95 | } 96 | 97 | 98 | ); 99 | }; 100 | 101 | /** 102 | * Get App State 103 | * 104 | * Get the requried state for the component from the Redux store. 105 | * 106 | * @param {Object} state Application state from Redux. 107 | */ 108 | const mapStateToProps = state => { 109 | return { 110 | languageData: state.language.languageData 111 | } 112 | } 113 | 114 | // Export the Language Dropdown Component. 115 | export default connect(mapStateToProps, { setLanguage })(LanguageDropdown); -------------------------------------------------------------------------------- /frontend/src/components/NavDropdown/LanguageDropdown/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import LanguageDropdown from "./LanguageDropdown"; 3 | 4 | // Export modules 5 | export default LanguageDropdown; -------------------------------------------------------------------------------- /frontend/src/components/NavDropdown/MemberNavDropdown/MemberLinks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "icon": "clipboard-list", 4 | "text": [ 5 | "common", 6 | "todoList" 7 | ], 8 | "link": [ 9 | "todo", 10 | "list" 11 | ] 12 | }, 13 | { 14 | "icon": "user-tie", 15 | "text": [ 16 | "common", 17 | "profile" 18 | ], 19 | "link": [ 20 | "user", 21 | "profile" 22 | ] 23 | }, 24 | { 25 | "icon": "user-shield", 26 | "text": [ 27 | "common", 28 | "twoFactorSettings" 29 | ], 30 | "link": [ 31 | "user", 32 | "twoFactor" 33 | ] 34 | }, 35 | { 36 | "icon": "user-lock", 37 | "text": [ 38 | "common", 39 | "changePassword" 40 | ], 41 | "link": [ 42 | "user", 43 | "changePassword" 44 | ] 45 | }, 46 | { 47 | "icon": "power-off", 48 | "logout": true, 49 | "text": [ 50 | "common", 51 | "logout" 52 | ] 53 | } 54 | ] -------------------------------------------------------------------------------- /frontend/src/components/NavDropdown/MemberNavDropdown/MemberNavDropdown.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { Link } from "react-router-dom"; 4 | import { get, map } from "lodash"; 5 | import { connect } from "react-redux"; 6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 7 | import { 8 | DropdownMenu, 9 | DropdownItem 10 | } from "reactstrap"; 11 | 12 | // Application Links 13 | import { links } from "../../../config"; 14 | import memberLinks from "./MemberLinks.json" 15 | 16 | // Redux Actions 17 | import { logoutUser } from "../../../redux/actions"; 18 | 19 | // Toast 20 | import Toasty from "../../../util/Toasty"; 21 | 22 | // History 23 | import History from "../../../util/History"; 24 | 25 | // Controllers 26 | import LogoutController from "../../../util/Logout"; 27 | 28 | /** 29 | * Member Navbar Component 30 | * 31 | * Contains the display components and associated functions for the member 32 | * dropdown for the main navbar. 33 | * 34 | * @param {Function} logoutUser Redux action to unset the user. 35 | * @param {String} locale The current locale of the application. 36 | * @param {Object} languageData Current language information for the app. Language data object. 37 | */ 38 | const MemberNavDropdown = ({ logoutUser, locale, languageData }) => { 39 | /** 40 | * Handle logout 41 | * 42 | * Will handle the logout request for when the button is clicked. 43 | * 44 | * @param {object} e Event object 45 | */ 46 | const logout = e => { 47 | // Prevent the link from executing. 48 | e.preventDefault(); 49 | 50 | // Use the LogoutController to handle the logout. 51 | LogoutController.logout(locale, languageData) 52 | .then(() => { 53 | // Call the logout function. 54 | logoutUser(); 55 | 56 | // Redirect the user to the home page. 57 | History.push(links.home); 58 | }).catch(error => 59 | // Display the error from the API server on logout. 60 | Toasty.notify({ 61 | type: Toasty.error(), 62 | content: error 63 | }) 64 | ); 65 | } 66 | 67 | // Display the Member Nav Dropdown. 68 | return ( 69 | 70 | 71 |
    { get(languageData, ["common", "welcome"]) }
    72 |
    73 | { 74 | map(memberLinks, (link, key) => ( 75 | 76 |   77 | { get(languageData, link["text"]) } 78 | 79 | )) 80 | } 81 |
    82 | ); 83 | }; 84 | 85 | /** 86 | * Get App State 87 | * 88 | * Get the requried state for the component from the Redux store. 89 | * 90 | * @param {Object} state Application state from Redux. 91 | */ 92 | const mapStateToProps = state => { 93 | return { 94 | locale: state.language.locale, 95 | languageData: state.language.languageData 96 | } 97 | } 98 | 99 | // Export the Member Navbar Component. 100 | export default connect(mapStateToProps, { logoutUser })(MemberNavDropdown); -------------------------------------------------------------------------------- /frontend/src/components/NavDropdown/MemberNavDropdown/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import MemberNavDropdown from "./MemberNavDropdown"; 3 | 4 | // Export modules 5 | export default MemberNavDropdown; -------------------------------------------------------------------------------- /frontend/src/components/Navbar/Navbar.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React, { useState } from "react"; 3 | import { get } from "lodash"; 4 | import { connect } from "react-redux"; 5 | import { Link } from "react-router-dom"; 6 | import { 7 | Collapse, 8 | NavbarBrand, 9 | Navbar, 10 | Nav, 11 | UncontrolledDropdown, 12 | DropdownToggle, 13 | Media, 14 | Container, 15 | Row, 16 | Col 17 | } from "reactstrap"; 18 | 19 | // Icons 20 | import appIcon from "../../assets/img/brand/appIcon.png"; 21 | 22 | // Components 23 | import LanguageDropdown from "../NavDropdown/LanguageDropdown"; 24 | import GuestNavDropdown from "../NavDropdown/GuestNavDropdown"; 25 | import MemberNavDropdown from "../NavDropdown/MemberNavDropdown"; 26 | 27 | /** 28 | * Navbar Component 29 | * 30 | * The component includes the Navbar component and functions for the 31 | * Navbar at the top of the page. 32 | * 33 | * @param {Object} languageData Current language information for the app. Language data object. 34 | * @param {Object} user User data for the logged in user. 35 | * @param {object} props Properties passed to the component from the parent. 36 | */ 37 | const DashNavbar = ({ languageData, user, ...props}) => { 38 | // React hook used to determine whether or not the menu should be collapsed 39 | // or shown to the user. 40 | const [collapseOpen, setCollapse] = useState(false); 41 | 42 | /** 43 | * Toggle menu collapse 44 | * 45 | * Toggles collapse between opened and closed (true/false). 46 | */ 47 | const toggleCollapse = () => { 48 | setCollapse(!collapseOpen); 49 | }; 50 | 51 | // Display the Navbar. 52 | return ( 53 | 54 | 55 | 62 | 63 | { get(languageData, ["common", "siteTitle"]) } 64 | 65 | 66 |
    67 | 68 | 69 | 77 | 78 | 79 |
    80 | 109 |
    110 |
    111 |
    112 | ); 113 | }; 114 | 115 | /** 116 | * Get App State 117 | * 118 | * Get the requried state for the component from the Redux store. 119 | * 120 | * @param {Object} state Application state from Redux. 121 | */ 122 | const mapStateToProps = state => { 123 | return { 124 | languageData: state.language.languageData, 125 | user: state.user.info 126 | } 127 | } 128 | 129 | // Export the Navbar Component. 130 | export default connect(mapStateToProps)(DashNavbar); 131 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Navbar from "./Navbar"; 3 | 4 | // Export modules 5 | export default Navbar; -------------------------------------------------------------------------------- /frontend/src/components/Util/Authenticating/Authenticating.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { get } from "lodash"; 4 | import { connect } from "react-redux"; 5 | import { 6 | Card, 7 | CardHeader, 8 | CardBody, 9 | Container, 10 | Row, 11 | Col 12 | } from "reactstrap"; 13 | 14 | /** 15 | * Authenticating Component 16 | * 17 | * Display a message to the user, in their preferred language, that they 18 | * are being authenticated to access the information. 19 | * 20 | * @param {Object} languageData Current language information for the app. Language data object. 21 | */ 22 | const Authenticating = ({ languageData }) => ( 23 | 24 | 25 | 26 | 27 | 28 | { get(languageData, ["common", "security"]) } 29 | 30 | 31 | { get(languageData, ["common", "authenticating"]) }... 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | 39 | /** 40 | * Get App State 41 | * 42 | * Get the requried state for the component from the Redux store. 43 | * 44 | * @param {Object} state Application state from Redux. 45 | */ 46 | const mapStateToProps = state => { 47 | return { 48 | languageData: state.language.languageData 49 | } 50 | } 51 | 52 | // Export the Loading Component. 53 | export default connect(mapStateToProps)(Authenticating); -------------------------------------------------------------------------------- /frontend/src/components/Util/Authenticating/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Authenticating from "./Authenticating"; 3 | 4 | // Export modules 5 | export default Authenticating; -------------------------------------------------------------------------------- /frontend/src/components/Util/Loading/Loading.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | 5 | /** 6 | * Loading Component 7 | * 8 | * A simple Font Awesome spinning icon to indicate to the user that 9 | * content is loading for them. 10 | */ 11 | const Loading = ({ size = "3x" }) => ( 12 | 13 | ); 14 | 15 | // Export the Loading Component. 16 | export default Loading; 17 | -------------------------------------------------------------------------------- /frontend/src/components/Util/Loading/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Loading from "./Loading"; 3 | 4 | // Export modules 5 | export default Loading; -------------------------------------------------------------------------------- /frontend/src/components/Util/NotAuthorized/NotAuthorized.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { get } from "lodash"; 4 | import { connect } from "react-redux"; 5 | import { 6 | Card, 7 | CardHeader, 8 | CardBody, 9 | Container, 10 | Row, 11 | Col 12 | } from "reactstrap"; 13 | 14 | /** 15 | * Authenticating Component 16 | * 17 | * Display a message to the user, in their preferred language, that 18 | * they do not have access to the content. 19 | * 20 | * @param {Object} languageData Current language information for the app. Language data object. 21 | */ 22 | const NotAuthorized = ({ languageData }) => ( 23 | 24 | 25 | 26 | 27 | 28 | { get(languageData, ["common", "notAuthorized", "title"]) } 29 | 30 | 31 | { get(languageData, ["common", "notAuthorized", "message"]) } 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | 39 | /** 40 | * Get App State 41 | * 42 | * Get the requried state for the component from the Redux store. 43 | * 44 | * @param {Object} state Application state from Redux. 45 | */ 46 | const mapStateToProps = state => { 47 | return { 48 | languageData: state.language.languageData 49 | } 50 | } 51 | 52 | // Export the Loading Component. 53 | export default connect(mapStateToProps)(NotAuthorized); 54 | -------------------------------------------------------------------------------- /frontend/src/components/Util/NotAuthorized/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import NotAuthorized from "./NotAuthorized"; 3 | 4 | // Export modules 5 | export default NotAuthorized; -------------------------------------------------------------------------------- /frontend/src/components/Util/PageData/PageData.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { get } from "lodash"; 4 | 5 | // Components 6 | import Loading from "../../../components/Util/Loading"; 7 | 8 | /** 9 | * Page Data Component 10 | * 11 | * Display page data for different pages with the ability to display a loading 12 | * icon during the loading phase, an error message, or the component intended to 13 | * be displayed. 14 | * 15 | * @param {Object} results Page results 16 | * @param {Boolean} isLoading Loading indication for API request. 17 | * @param {Object} hasError Error object from the API request. 18 | * @param {Object} component Component to be displayed. 19 | */ 20 | const PageData = ({ results, isLoading, hasError, component }) => { 21 | // Display errors or the loading message. 22 | const loadingOrErrors = hasError ? <>{ hasError.data.message } : ; 23 | 24 | // Check if the results are anything but 200 to display their error message. 25 | const isResults200 = get(results, "status") === 200 ? component : <>{ get(results, "data") && results.data.message }; 26 | 27 | // Return loading, errors, or the page component. 28 | return isLoading ? loadingOrErrors : isResults200; 29 | } 30 | 31 | // Export the User Profile Page Data Component. 32 | export default PageData; -------------------------------------------------------------------------------- /frontend/src/components/Util/PageData/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import PageData from "./PageData"; 3 | 4 | // Export modules 5 | export default PageData; -------------------------------------------------------------------------------- /frontend/src/config/config.jsx: -------------------------------------------------------------------------------- 1 | // Config object 2 | const config = { 3 | fusionAuth: { 4 | // FusionAuth URL: http://localhost:9011 5 | BASEURL: "http://localhost:9011", 6 | // Application ID from FusionAuth 7 | APPLICATION_ID: "10e4d908-7655-44af-abf0-1a031aff519a" 8 | }, 9 | apiServer: { 10 | // API Server URL: http://localhost:5000 11 | BASEURL: "http://localhost:5000" 12 | }, 13 | app: { 14 | // Name for the 2Factor application: FusionAuth 15 | TWO_FA_NAME: "fusionAuthDemoApp" 16 | } 17 | }; 18 | 19 | // Export the application config. 20 | export default config; -------------------------------------------------------------------------------- /frontend/src/config/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import config from "./config"; 3 | import links from "./links"; 4 | 5 | // Export modules 6 | export { 7 | config, 8 | links 9 | }; -------------------------------------------------------------------------------- /frontend/src/config/links.jsx: -------------------------------------------------------------------------------- 1 | // Export the application links. 2 | export default { 3 | home: "/", 4 | auth: { 5 | login: "/auth/login/", 6 | register: "/auth/register/", 7 | verifyEmail: "/auth/verify/email/", 8 | twoFactor: "/auth/verify/2fa/", 9 | forgotPassword: "/auth/forgotPassword/", 10 | changePassword: "/auth/changePassword/" 11 | }, 12 | user: { 13 | profile: "/user/profile/", 14 | editProfile: "/user/profile/edit/", 15 | changePassword: "/user/changePassword/", 16 | twoFactor: "/user/2fa/" 17 | }, 18 | todo: { 19 | list: "/todo/", 20 | add: "/todo/add/", 21 | edit: "/todo/edit/" 22 | } 23 | }; -------------------------------------------------------------------------------- /frontend/src/data/language/languages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "English", 4 | "shortcode": "en", 5 | "flag": "/assets/images/flags/en.png" 6 | }, 7 | { 8 | "name": "Español", 9 | "shortcode": "es", 10 | "flag": "/assets/images/flags/es.png" 11 | } 12 | ] -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | // Components 7 | import App from "./components/App"; 8 | 9 | // Render the application to the page. 10 | ReactDOM.render(, document.getElementById("root")); 11 | 12 | // If you want your app to work offline and load faster, you can change 13 | // unregister() to register() below. Note this comes with some pitfalls. 14 | // Learn more about service workers: https://bit.ly/CRA-PWA 15 | serviceWorker.unregister(); 16 | -------------------------------------------------------------------------------- /frontend/src/layouts/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import DashLayout from "./DashLayout"; 3 | 4 | // Export modules 5 | export default DashLayout; -------------------------------------------------------------------------------- /frontend/src/redux/actionTypes/actionTypes.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Redux Action Types 3 | * 4 | * Available action types for Redux dispatch operations 5 | * for the application. 6 | */ 7 | 8 | // Language 9 | const SET_LANGUAGE = "SET_LANGUAGE"; 10 | 11 | // User 12 | const SET_USER = "SET_USER"; 13 | const LOGOUT_USER = "LOGOUT_USER"; 14 | 15 | // Eexport the action types. 16 | export { 17 | SET_LANGUAGE, 18 | SET_USER, 19 | LOGOUT_USER 20 | }; -------------------------------------------------------------------------------- /frontend/src/redux/actionTypes/index.jsx: -------------------------------------------------------------------------------- 1 | // Import action types. 2 | import { 3 | SET_LANGUAGE, 4 | SET_USER, 5 | LOGOUT_USER 6 | } from "./actionTypes"; 7 | 8 | // Export the action types. 9 | export { 10 | SET_LANGUAGE, 11 | SET_USER, 12 | LOGOUT_USER 13 | }; -------------------------------------------------------------------------------- /frontend/src/redux/actions/index.jsx: -------------------------------------------------------------------------------- 1 | // Import actions. 2 | import { setLanguage } from "./language"; 3 | import { setUser, logoutUser } from "./user"; 4 | 5 | // Export actions. 6 | export { 7 | setLanguage, 8 | setUser, 9 | logoutUser 10 | }; -------------------------------------------------------------------------------- /frontend/src/redux/actions/language.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Language Actions 3 | * 4 | * Action creators for Language based actions. 5 | */ 6 | 7 | // Dependencies 8 | import { get } from "lodash"; 9 | 10 | // Language 11 | import Language from "../../util/Language"; 12 | 13 | // Toasty 14 | import Toasty from "../../util/Toasty"; 15 | 16 | // Action Types 17 | import { SET_LANGUAGE } from "../actionTypes"; 18 | 19 | /** 20 | * Set the App Language 21 | * 22 | * Performs a dispatch to the Redux store with the new language and 23 | * the language data. If a new language cannot be found, alert the user. 24 | * There's a very small percent chance that no HTML element would be clicked 25 | * that does not have the `data-lang` attribute. 26 | * 27 | * @param {Object} target The HTML element that 28 | */ 29 | const setLanguage = ({ target }) => (dispatch, getState) => { 30 | // Get the language attribute from the button and the span item. 31 | // With this setup, it's done this way so that either could be clicked. 32 | // If not, clicking on the text in the dropdown would result in an error. 33 | const newLanguage = target.getAttribute("data-lang"); 34 | 35 | // Make sure the language supplied to the function is not null / empty. 36 | if (!newLanguage) { 37 | // Get the language data. 38 | const languageData = getState("language").language.languageData; 39 | 40 | // Display a toast notification that the language was empty. 41 | Toasty.notify({ 42 | type: Toasty.error(), 43 | content: get(languageData, ["common", "changeLang", "choose"]) 44 | }); 45 | 46 | return; 47 | } 48 | 49 | // Get the language data for the new language. 50 | const languageData = Language.getLanguageData(newLanguage); 51 | 52 | // Dispatch the result. 53 | dispatch({ 54 | type: SET_LANGUAGE, 55 | payload: { 56 | locale: newLanguage, 57 | languageData 58 | } 59 | }); 60 | } 61 | 62 | // Export the actions. 63 | export { 64 | setLanguage 65 | } -------------------------------------------------------------------------------- /frontend/src/redux/actions/user.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * User Actions 3 | * 4 | * Action creators for User based actions. 5 | */ 6 | 7 | // Dependencies 8 | import { get } from "lodash"; 9 | 10 | // Toasty 11 | import Toasty from "../../util/Toasty"; 12 | 13 | // Action Types 14 | import { SET_USER, LOGOUT_USER } from "../actionTypes"; 15 | 16 | /** 17 | * Set the User Object 18 | * 19 | * Attempt to set the user object with data about the user. 20 | * 21 | * @param {Object} user The user object returned from FusionAuth /api/login. 22 | */ 23 | const setUser = user => (dispatch, getState) => { 24 | // Grab only the pertinent information to send to storage. We don't want everything. 25 | const savedUser = { 26 | email: user.email, 27 | username: user.username, 28 | firstName: user.firstName, 29 | lastName: user.lastName 30 | } 31 | 32 | // Make sure the user supplied to the function is not null / empty. 33 | if (!user) { 34 | // Get the language data. 35 | const languageData = getState("language").language.languageData; 36 | 37 | // Return with an error message for a toast event. 38 | return Toasty.notify({ 39 | toastType: Toasty.error(), 40 | toastMessage: get(languageData, ["common", "auth", "noUser"]) 41 | }); 42 | } 43 | 44 | // Dispatch the result. 45 | dispatch({ 46 | type: SET_USER, 47 | payload: { 48 | info: savedUser 49 | } 50 | }); 51 | } 52 | 53 | /** 54 | * Logout User 55 | * 56 | * Logs out the user by using the LOGOUT_USER action type. This will 57 | * set the user.info object to null. 58 | */ 59 | const logoutUser = () => (dispatch, getState) => { 60 | // Get the language data. 61 | const languageData = getState("language").language.languageData; 62 | 63 | // The user just logged out, so let them know. 64 | Toasty.notify({ 65 | type: Toasty.success(), 66 | content: get(languageData, ["common", "auth", "loggedOut"]) 67 | }); 68 | 69 | // Dispatch the result. 70 | dispatch({ 71 | type: LOGOUT_USER 72 | }); 73 | } 74 | 75 | // Export the actions. 76 | export { 77 | setUser, 78 | logoutUser 79 | } -------------------------------------------------------------------------------- /frontend/src/redux/reducers/index.jsx: -------------------------------------------------------------------------------- 1 | // Import reducer. 2 | import rootReducer from "./rootReducer"; 3 | 4 | // Export reducer. 5 | export default rootReducer; -------------------------------------------------------------------------------- /frontend/src/redux/reducers/languageReducer.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Language Reducer 3 | * 4 | * Creates a Redux reducer for handling language actions. 5 | */ 6 | 7 | // Action Types 8 | import { SET_LANGUAGE } from "../actionTypes"; 9 | 10 | // Language Data 11 | import common_en from "../../data/language/en/common.json"; 12 | 13 | // Setup initial state with the English language. 14 | const initialState = { 15 | locale: "en", 16 | languageData: { 17 | common: common_en 18 | } 19 | }; 20 | 21 | // Export the Language Reducer. 22 | export default (state = initialState, action) => { 23 | switch (action.type) { 24 | case SET_LANGUAGE: { 25 | const { locale, languageData } = action.payload; 26 | 27 | return { 28 | ...state, 29 | locale, 30 | languageData 31 | }; 32 | } 33 | default: 34 | return state; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/redux/reducers/rootReducer.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Root Reducer 3 | * 4 | * Creates a Redux root reducer for handling all reducer actions in the 5 | * application. 6 | */ 7 | 8 | // Dependencies 9 | import { combineReducers } from "redux"; 10 | 11 | // Reducers 12 | import language from "./languageReducer"; 13 | import user from "./userReducer"; 14 | 15 | // Export the Redux reducer. 16 | export default combineReducers({ 17 | language, 18 | user 19 | }); -------------------------------------------------------------------------------- /frontend/src/redux/reducers/userReducer.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * User Reducer 3 | * 4 | * Creates a Redux reducer for handling user actions. 5 | */ 6 | 7 | // Action Types 8 | import { SET_USER, LOGOUT_USER } from "../actionTypes"; 9 | 10 | // Setup initial state with an empty user info object. 11 | const initialState = { 12 | info: null 13 | }; 14 | 15 | // Export the User Reducer. 16 | export default (state = initialState, action) => { 17 | switch (action.type) { 18 | case SET_USER: { 19 | const { info } = action.payload; 20 | 21 | return { 22 | info 23 | }; 24 | } 25 | case LOGOUT_USER: { 26 | return { 27 | info: null 28 | }; 29 | } 30 | default: 31 | return state; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/redux/store/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import { store, persistor } from "./store"; 3 | 4 | // Export modules 5 | export { 6 | store, 7 | persistor 8 | }; -------------------------------------------------------------------------------- /frontend/src/redux/store/store.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Redux Store 3 | * 4 | * Creates a Redux store for the application and passes the root reducer 5 | * to the store. Also applies the thunk middleware so that actions can 6 | * be dispatched asynchronously. 7 | */ 8 | 9 | // Dependencies 10 | import { createStore, applyMiddleware } from "redux"; 11 | import thunk from "redux-thunk"; 12 | import { persistStore, persistReducer } from "redux-persist"; 13 | import storage from "redux-persist/lib/storage"; 14 | 15 | // Reducers 16 | import rootReducer from "../reducers"; 17 | 18 | // Redux Persistence Config. 19 | const persistConfig = { 20 | key: "FA-Demo", 21 | storage 22 | } 23 | 24 | // Reducer for persisted config. 25 | const persistedReducer = persistReducer(persistConfig, rootReducer); 26 | 27 | // Create the Redux store. 28 | const store = createStore(persistedReducer, applyMiddleware(thunk)); 29 | const persistor = persistStore(store); 30 | 31 | // Export the Redux store. 32 | export { 33 | store, 34 | persistor 35 | }; -------------------------------------------------------------------------------- /frontend/src/routes.js: -------------------------------------------------------------------------------- 1 | // Views 2 | import Home from "./views/Home"; 3 | import Login from "./views/Auth/Login"; 4 | import Register from "./views/Auth/Register"; 5 | import ForgotPassword from "./views/Auth/ForgotPassword"; 6 | import ChangePassword from "./views/Auth/ChangePassword"; 7 | import { VerifyEmail, Verify2Factor } from "./views/Auth/Verify"; 8 | import { UserChangePassword, User2Factor } from "./views/User"; 9 | import { UserProfileView, UserProfileEdit } from "./views/User/UserProfile"; 10 | import { TodoList, TodoModify, TodoView } from "./views/Todo"; 11 | 12 | /** 13 | * Routes Array 14 | * 15 | * The routes array contains information about all of the available routes 16 | * in the application and is used to determine what text to display as well 17 | * as whether or not the route requires authentication to view. 18 | */ 19 | const routes = [ 20 | { 21 | path: "/auth/login/", 22 | name: "Login", 23 | component: Login 24 | }, { 25 | path: "/auth/register/", 26 | name: "Register", 27 | component: Register 28 | }, { 29 | path: "/auth/forgotPassword/", 30 | name: "Forgot Password", 31 | component: ForgotPassword 32 | }, { 33 | path: "/auth/ChangePassword/", 34 | name: "Change Password", 35 | component: ChangePassword 36 | }, { 37 | path: "/auth/ChangePassword/:changePasswordId", 38 | name: "Change Password", 39 | component: ChangePassword 40 | }, { 41 | path: "/auth/verify/email/", 42 | name: "Verify Email", 43 | component: VerifyEmail 44 | }, { 45 | path: "/auth/verify/email/:verificationId", 46 | name: "Verify Email", 47 | component: VerifyEmail 48 | }, { 49 | path: "/auth/verify/2fa/", 50 | name: "2FA Login", 51 | component: Verify2Factor 52 | }, { 53 | path: "/", 54 | name: "Home", 55 | component: Home 56 | }, { 57 | private: true, 58 | path: "/user/profile/", 59 | name: "User Profile", 60 | component: UserProfileView 61 | }, { 62 | private: true, 63 | path: "/user/profile/edit/", 64 | name: "Edit User Profile", 65 | component: UserProfileEdit 66 | }, { 67 | private: true, 68 | path: "/user/changePassword/", 69 | name: "User Change Password", 70 | component: UserChangePassword 71 | }, { 72 | private: true, 73 | path: "/user/2fa/", 74 | name: "User 2FA", 75 | component: User2Factor 76 | }, { 77 | private: true, 78 | path: "/todo/", 79 | name: "Todo List", 80 | component: TodoList 81 | }, { 82 | private: true, 83 | path: "/todo/add/", 84 | name: "Add ToDo", 85 | component: TodoModify 86 | }, { 87 | private: true, 88 | path: "/todo/edit/:id", 89 | name: "Edit ToDo", 90 | component: TodoModify 91 | }, { 92 | private: true, 93 | path: "/todo/:id", 94 | name: "View ToDo", 95 | component: TodoView 96 | } 97 | ]; 98 | 99 | // Export the routes array. 100 | export default routes; 101 | -------------------------------------------------------------------------------- /frontend/src/util/APIFetch/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import APIFetch from "./APIFetch"; 3 | 4 | // Export modules 5 | export default APIFetch; -------------------------------------------------------------------------------- /frontend/src/util/Auth/Auth.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Auth Module 3 | * 4 | * Contains function related to authentication within the application. It handles 5 | * login, logout, and page access determination for the frontend. It also manages 6 | * the user state so that useful information about the user can be displayed without 7 | * the need to query the backend or FusionAuth. 8 | */ 9 | 10 | // Dependencies 11 | import axios from "axios"; 12 | import { get } from "lodash"; 13 | 14 | // Config 15 | import { config, links } from "../../config"; 16 | 17 | // History 18 | import History from "../History"; 19 | 20 | // Auth API endpoints 21 | import AuthAPI from "./AuthAPI"; 22 | 23 | // Declare the Auth Module. 24 | const Auth = {}; 25 | 26 | /** 27 | * Determine page access 28 | * 29 | * This checks whether or not a user is able to view a certain page, and this is 30 | * called before allowing the user to see the content of the page. This is independent 31 | * of any API endpoint call to obtain information for the page itself. This is used 32 | * over local storage to remove the frontend's ability to determine access. 33 | * 34 | * @param {Function} logoutUser Redux action to unset the user. 35 | * @param {string} path The path of the URL the user is trying to access. 36 | * @param {String} locale The current locale of the application. 37 | * @param {Object} languageData Current language information for the app. Language data object. 38 | */ 39 | Auth.canAccessPage = ({ logoutUser, path, locale, languageData }) => { 40 | // Make a request to the API server and return a promise with the result. 41 | return new Promise((resolve, reject) => { 42 | axios({ 43 | baseURL: config.apiServer.BASEURL, 44 | url: AuthAPI.canAccessPage.PATH_SEARCH, 45 | method: AuthAPI.canAccessPage.PATH_METHOD, 46 | withCredentials: true, 47 | data: { 48 | path 49 | }, 50 | headers: { 51 | locale 52 | } 53 | }).then(response => 54 | // If the status is 200, then the user has access, otherwise, deny access. 55 | response.status === 200 ? resolve() : reject() 56 | ).catch(({ response }) => { 57 | // Check if the response object exists (it will not exist if the API service 58 | // cannot be reached). 59 | if (!response) { 60 | return reject(get(languageData, ["common", "apiDown"])); 61 | } 62 | 63 | // If the the API service says that we need to login again, do so. 64 | if (response.data.loginAgain) { 65 | logoutUser(); 66 | 67 | // Redirect the user to the home page. 68 | History.push(links.home); 69 | } 70 | 71 | // Return the error message. 72 | return reject(response.data); 73 | }); 74 | }); 75 | }; 76 | 77 | // Export the Auth Controller. 78 | export default Auth; -------------------------------------------------------------------------------- /frontend/src/util/Auth/AuthAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the Auth API. 2 | export default { 3 | canAccessPage: { 4 | PATH_SEARCH: "/api/roles/canAccessPage", 5 | PATH_METHOD: "post" 6 | } 7 | }; -------------------------------------------------------------------------------- /frontend/src/util/Auth/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Auth from "./Auth"; 3 | 4 | // Export modules 5 | export default Auth; -------------------------------------------------------------------------------- /frontend/src/util/FormHandler/FormHandler.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Form Handler 3 | * 4 | * Contains the methods for handling API responses and providing the proper 5 | * redirect link or error type and message for either form based alerts, or 6 | * for the Toasty module. 7 | */ 8 | 9 | // Dependencies 10 | import { get, isEmpty } from "lodash"; 11 | import classNames from "classnames"; 12 | 13 | // Declare the Response Handler 14 | const FormHandler = {}; 15 | 16 | /** 17 | * Get the class names for an input 18 | * 19 | * Determines the proper class names for an input based on whether it is 20 | * in a success or error state. 21 | * 22 | * @param {Object} formData Collection of input data from the form being checked. 23 | * @param {String} input Name of the input 24 | */ 25 | FormHandler.inputStatus = (formData, input) => { 26 | // Get the error state of the input. 27 | const error = get(formData, [input, "error"]); 28 | 29 | // Return the class name for the given state. 30 | if (error === true) { 31 | return classNames("is-invalid"); 32 | } else if (error === false) { 33 | return classNames("is-valid"); 34 | } else { 35 | // Defaults to neither success nor error state. 36 | return classNames(); 37 | } 38 | }; 39 | 40 | /** 41 | * Validate form inputs 42 | * 43 | * Perform input validation on form blur and update the `formData` as necessary. 44 | * 45 | * @param {String || Array} form String or array for path to the language data for the input. 46 | * @param {String} target Name of the input to set validation info for. 47 | * @param {Array} error Error list for the input. 48 | * @param {Object} languageData Current language information for the app. Language data object. 49 | */ 50 | FormHandler.validate = (form, target, error, languageData) => { 51 | // Setup variables for setting state. 52 | let isError, errorText, validText; 53 | 54 | // Check if there is an error with the input. Set the state variables appropriately. 55 | if (!isEmpty(error)) { 56 | isError = true; 57 | errorText = error; 58 | validText = ""; 59 | } else { 60 | isError = false; 61 | errorText = ""; 62 | validText = get(languageData, ["common", ...form, target, "validText"]); 63 | } 64 | 65 | // Return the information. 66 | return { isError, errorText, validText }; 67 | }; 68 | 69 | // Export the Form Handler. 70 | export default FormHandler; -------------------------------------------------------------------------------- /frontend/src/util/FormHandler/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import FormHandler from "./FormHandler"; 3 | 4 | // Export modules 5 | export default FormHandler; -------------------------------------------------------------------------------- /frontend/src/util/History/History.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * History Utility 3 | * 4 | * This creates a representation of the history of the application 5 | * that can be utilized throughout the different components without the need 6 | * to pass the history property from the route (App -> Dashboard -> new component) 7 | * to each component. 8 | */ 9 | 10 | // Dependencies 11 | import { createBrowserHistory } from "history"; 12 | 13 | // Export the history module. 14 | export default createBrowserHistory(); -------------------------------------------------------------------------------- /frontend/src/util/History/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import History from "./History"; 3 | 4 | // Export modules 5 | export default History; -------------------------------------------------------------------------------- /frontend/src/util/Language/Language.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Language Module 3 | * 4 | * Contains function related to language methods for the application. Handles 5 | * getting the language data for the preferred language. 6 | */ 7 | 8 | // Languages 9 | import common_en from "../../data/language/en/common.json"; 10 | import common_es from "../../data/language/es/common.json"; 11 | 12 | // Declare the Language Module object. 13 | const Language = {}; 14 | 15 | /** 16 | * Get application language data 17 | * 18 | * Gets the application's language data based on the preferred language of the user. 19 | * This will be English by default. 20 | * 21 | * @param {String} preferredLanguage Preferred language for application 22 | */ 23 | Language.getLanguageData = preferredLanguage => { 24 | // Set an object that we'll fill in with the switch statement. 25 | let langData; 26 | 27 | // Check the preferred language against available languages. Default to English. 28 | switch (preferredLanguage) { 29 | case "es": 30 | langData = { 31 | common: common_es 32 | }; 33 | break; 34 | default: 35 | langData = { 36 | common: common_en 37 | }; 38 | } 39 | 40 | // Return the language data for the application. 41 | return langData; 42 | } 43 | 44 | // Export the Language Module. 45 | export default Language; -------------------------------------------------------------------------------- /frontend/src/util/Language/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Language from "./Language"; 3 | 4 | // Export modules 5 | export default Language; -------------------------------------------------------------------------------- /frontend/src/util/Logout/Logout.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Logout Module 3 | * 4 | * Contains functions related to logging out of the application. 5 | */ 6 | 7 | // Dependencies 8 | import axios from "axios"; 9 | import { get } from "lodash"; 10 | 11 | // Config 12 | import { config } from "../../config"; 13 | 14 | // Logout API endpoints 15 | import LogoutAPI from "./LogoutAPI"; 16 | 17 | // Declare the Logout Module. 18 | const LogoutController = {}; 19 | 20 | /** 21 | * Logout the current user 22 | * 23 | * Performs a logout by requesting the API service to remove the access token 24 | * and refresh token cookies that were set to keep the user logged in. 25 | * 26 | * @param {String} locale The current locale of the application. 27 | * @param {Object} languageData Current language information for the app. Language data object. 28 | */ 29 | LogoutController.logout = (locale, languageData) => { 30 | return new Promise((resolve, reject) => { 31 | // Make the request to the API service. 32 | axios({ 33 | baseURL: config.apiServer.BASEURL, 34 | url: LogoutAPI.apiService.getLogout.PATH_SEARCH, 35 | method: LogoutAPI.apiService.getLogout.PATH_METHOD, 36 | withCredentials: true, 37 | headers: { 38 | locale 39 | } 40 | }).then(() => resolve()) 41 | .catch(() => reject(get(languageData, ["common", "auth", "logoutError"]))); 42 | }); 43 | }; 44 | 45 | // Export the Logout Module. 46 | export default LogoutController; -------------------------------------------------------------------------------- /frontend/src/util/Logout/LogoutAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the Logout API. 2 | export default { 3 | apiService: { 4 | getLogout: { 5 | PATH_SEARCH: "/api/fusionAuth/logout", 6 | PATH_METHOD: "delete" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /frontend/src/util/Logout/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Logout from "./Logout"; 3 | 4 | // Export modules 5 | export default Logout; -------------------------------------------------------------------------------- /frontend/src/util/PrivateRoute/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Private Route Component 3 | * 4 | * This Private Route utilizes a connection to an API server to determine whether or not 5 | * a user is able to view a certain page. This works on a validated JWT as well as user 6 | * and application roles. Some may utilize local storage for this ability, however, this 7 | * route chooses a more stringent method to ensure users only access content they 8 | * should be able to access. This will add some load time to each page request due 9 | * to the network request. It's a trade off that you must consider. 10 | */ 11 | 12 | // Dependencies 13 | import React, { useState, useEffect } from "react"; 14 | import { Route } from "react-router-dom"; 15 | import { connect } from "react-redux"; 16 | 17 | // Components 18 | import Authenticating from "../../components/Util/Authenticating"; 19 | import NotAuthorized from "../../components/Util/NotAuthorized"; 20 | 21 | // Redux Actions 22 | import { logoutUser } from "../../redux/actions"; 23 | 24 | // Toast 25 | import Toasty from "../../util/Toasty"; 26 | 27 | // Authentication Methods 28 | import Auth from "../Auth"; 29 | 30 | /** 31 | * Private Route 32 | * 33 | * Checks whether or not the user is authenticated in order to access the private route. 34 | * If not, the user is redirected to the login page. 35 | * 36 | * @param {Object} component React component 37 | * @param {Function} logoutUser Redux action to unset the user. 38 | * @param {String} locale The current locale of the application. 39 | * @param {Object} languageData Current language information for the app. Language data object. 40 | */ 41 | const PrivateRoute = ({ component: Component, logoutUser, locale, languageData, ...rest }) => { 42 | // Setup initial state. 43 | const [canAccessPage, setCanAccessPage] = useState(null); 44 | 45 | // React hook 46 | useEffect(() => { 47 | // Set a variable so we can cancel the request if needed (ex, user 48 | // moves to a new page). 49 | let didCancel = false; 50 | 51 | /** 52 | * Determine if a user can access a page. 53 | * 54 | * Makes a call to the Auth Controller to see if a user can access the page 55 | * in question. It also makes sure that the user has not moved to a new page 56 | * before trying to update the state. 57 | */ 58 | const determineAccess = () => { 59 | // Make sure we don't try to change state after re-render. 60 | if (!didCancel) { 61 | Auth.canAccessPage({ locale, logoutUser, ...rest }) 62 | .then(() => !didCancel && setCanAccessPage(true)) 63 | .catch(response => { 64 | // Let the user know they cannot access the page. 65 | Toasty.notify({ 66 | type: Toasty.error(), 67 | content: response.message 68 | }); 69 | 70 | // Make sure we don't try to change state after re-render. 71 | if (!didCancel) { 72 | // Set the access page variable so the application can redirect the user. 73 | setCanAccessPage(false); 74 | } 75 | }); 76 | } 77 | }; 78 | 79 | // Call the page access method. 80 | determineAccess(); 81 | 82 | /** 83 | * Perform action when the component is unmounted 84 | * 85 | * The return function in useEffect is equivalent to componentWillUnmount and 86 | * can be used to cancel the API request. 87 | */ 88 | return () => { 89 | // Set the canceled variable to true. 90 | didCancel = true; 91 | }; 92 | 93 | // eslint-disable-next-line 94 | }, [Component]); 95 | 96 | // Render the Private Route, or send the user to the homepage. 97 | return 100 | canAccessPage !== null 101 | ? canAccessPage 102 | ? 103 | : 104 | : 105 | } 106 | /> 107 | }; 108 | 109 | /** 110 | * Get App State 111 | * 112 | * Get the requried state for the component from the Redux store. 113 | * 114 | * @param {Object} state Application state from Redux. 115 | */ 116 | const mapStateToProps = state => { 117 | return { 118 | locale: state.language.locale, 119 | languageData: state.language.languageData 120 | } 121 | } 122 | 123 | // Export the Private Route Component. 124 | export default connect(mapStateToProps, { logoutUser })(PrivateRoute); -------------------------------------------------------------------------------- /frontend/src/util/PrivateRoute/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import PrivateRoute from "./PrivateRoute"; 3 | 4 | // Export modules 5 | export default PrivateRoute; -------------------------------------------------------------------------------- /frontend/src/util/ResponseHandler/ResponseHandler.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Response Handler 3 | * 4 | * Contains the methods for handling API responses and providing the proper 5 | * redirect link or error type and message for either form based alerts, or 6 | * for the Toasty module. Also handles actions for API responses. 7 | */ 8 | 9 | // Dependencies 10 | import { get } from "lodash"; 11 | 12 | // History 13 | import History from "../History"; 14 | import Toasty from "../Toasty"; 15 | 16 | // Declare the Response Handler 17 | const ResponseHandler = {}; 18 | 19 | /** 20 | * Response Status 21 | * 22 | * Handle response statuses so that the views know how to handle them. Returns 23 | * a promise that resolves with a redirect link, or rejects with an error object. 24 | * 25 | * @param {Number} response The response from the API request. 26 | * @param {Boolean} apiMessage A boolean on whether or not to use the API response message. 27 | * @param {Object} languageData Current language information for the app. Current language information for the app. 28 | * @param {Array} langKey Key for which status code message to display 29 | * @param {Object} alertData Status codes and their respective alert type. 30 | * @param {Object} redirectData Status codes and their respective redirect link. 31 | */ 32 | ResponseHandler.status = ({ response, apiMessage, languageData, langKey, alertData, redirectData = {} }) => { 33 | // Get the status from the response. 34 | const status = response.status; 35 | 36 | // Return a promise. 37 | return new Promise((resolve, reject) => { 38 | // Check if the status is in the redirect object. 39 | if (redirectData.hasOwnProperty(status)) { 40 | // Check if the redirect requires a Toast notification. 41 | if (redirectData[status].hasOwnProperty("alert")) { 42 | // Get the proper message for the Toast notification. 43 | const message = apiMessage 44 | ? response.data.message 45 | : get(languageData, [...langKey, status]) 46 | ? get(languageData, [...langKey, status]) 47 | : get(languageData, [...langKey, 500]); 48 | 49 | Toasty.notify({ 50 | type: Toasty.info(), 51 | content: message 52 | }) 53 | } 54 | 55 | // Resolve with the link to redirect to. 56 | resolve({ 57 | redirect: redirectData[status]["redirect"] 58 | }); 59 | } else { 60 | // Get the alert type if it exists, or default to status 500 alert type. 61 | const alertType = alertData.hasOwnProperty(status) ? alertData[status] : alertData[500]; 62 | 63 | // Check if we should return the response from the API or find the response in the 64 | // language files. 65 | if (apiMessage) { 66 | reject({ 67 | type: alertType, 68 | content: response.data.message 69 | }); 70 | } else { 71 | // Check if the language key exists for the status to reject an error object 72 | // with the message for the status or 500, and the alert type. 73 | if (get(languageData, [...langKey, status])) { 74 | reject({ 75 | type: alertType, 76 | content: get(languageData, [...langKey, status]) 77 | }); 78 | } else { 79 | reject({ 80 | type: alertType, 81 | content: get(languageData, [...langKey, 500]) 82 | }); 83 | } 84 | } 85 | } 86 | }); 87 | }; 88 | 89 | /** 90 | * Handle app actions 91 | * 92 | * A custom action handler that handles redirects and errors for the user. 93 | * 94 | * @param {Object} responseObject The object response with alert info or redirect info. 95 | * @param {Object} data Data object required for handling the action. 96 | * @param {Function} action Redux action to handle data. 97 | */ 98 | ResponseHandler.action = (responseObject, data, action) => { 99 | if (responseObject.redirect) { 100 | // If the redirect is to the homepage, then the user has been logged in, so 101 | // we must log the user in on the frontend. 102 | if (responseObject.setUser && data.user) { 103 | // Use the redux action to handle the user object. 104 | action(data.user); 105 | } 106 | 107 | // Redirect the user to the location in the response object. 108 | History.push({ 109 | pathname: responseObject.redirect, 110 | state: { 111 | twoFactorId: get(data, "twoFactorId"), 112 | changePasswordId: get(data, "changePasswordId"), 113 | from: get(responseObject, "from"), 114 | displayMessage: true 115 | } 116 | }); 117 | } else { 118 | // Return with the responseObject since there was an error. 119 | return responseObject; 120 | } 121 | }; 122 | 123 | // Export the Response Handler. 124 | export default ResponseHandler; -------------------------------------------------------------------------------- /frontend/src/util/ResponseHandler/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import ResponseHandler from "./ResponseHandler"; 3 | 4 | // Export modules 5 | export default ResponseHandler; -------------------------------------------------------------------------------- /frontend/src/util/Toasty/Toasty.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Toast Utility 3 | * 4 | * Contains the information about the "react-toastify" package and maintains 5 | * the ability to create toasts utilizing that package. These toasts are helpful 6 | * for displaying temporary information to users. 7 | */ 8 | 9 | // Dependencies 10 | import { toast } from "react-toastify"; 11 | 12 | // Declare the Toasty Utility. 13 | const Toasty = {}; 14 | 15 | /** 16 | * Toast Notify 17 | * 18 | * Create a toast notification with the information supplied, or the defaults 19 | * specified below. This is used for creating useful temporary notifications where 20 | * you may not want to "permanently" display something on the screen, like the "Alert" 21 | * from bootstrap / reactstrap. 22 | */ 23 | Toasty.notify = ({ type, content, position, autoClose }) => { 24 | // Create a toast. 25 | toast(content, { 26 | position: position || "top-right", 27 | type: type || Toasty.info(), 28 | autoClose: autoClose || 3000 29 | }); 30 | }; 31 | 32 | /** 33 | * Toast Default 34 | * 35 | * Returns the Toast default type. 36 | */ 37 | Toasty.default = () => { 38 | return toast.TYPE.DEFAULT; 39 | }; 40 | 41 | /** 42 | * Toast Info 43 | * 44 | * Returns the Toast info type. 45 | */ 46 | Toasty.info = () => { 47 | return toast.TYPE.INFO; 48 | }; 49 | 50 | /** 51 | * Toast Success 52 | * 53 | * Returns the Toast success type. 54 | */ 55 | Toasty.success = () => { 56 | return toast.TYPE.SUCCESS; 57 | }; 58 | 59 | /** 60 | * Toast Warning 61 | * 62 | * Returns the Toast warning type. 63 | */ 64 | Toasty.warning = () => { 65 | return toast.TYPE.WARNING; 66 | }; 67 | 68 | /** 69 | * Toast Error 70 | * 71 | * Returns the Toast error type. 72 | */ 73 | Toasty.error = () => { 74 | return toast.TYPE.ERROR; 75 | }; 76 | 77 | // Export the Toasty Utility. 78 | export default Toasty; -------------------------------------------------------------------------------- /frontend/src/util/Toasty/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Toasty from "./Toasty"; 3 | 4 | // Export modules 5 | export default Toasty; -------------------------------------------------------------------------------- /frontend/src/util/ValidateInput/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import ValidateInput from "./ValidateInput"; 3 | 4 | // Export modules 5 | export default ValidateInput; -------------------------------------------------------------------------------- /frontend/src/views/Auth/ChangePassword/ChangePasswordAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the Change Password API. 2 | export default { 3 | apiService: { 4 | postChangePassword: { 5 | PATH_SEARCH: "/api/fusionAuth/changePassword", 6 | PATH_METHOD: "post" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/Auth/ChangePassword/ChangePasswordController.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Change Password Controller 3 | * 4 | * Contains functions related to handling Change Password responses and actions. 5 | * Enables redirecting the user depending on the response from the API 6 | * service. Also provides alert data for the view if needed. 7 | */ 8 | 9 | // Dependencies 10 | import { get } from "lodash"; 11 | 12 | // Config & Application Links 13 | import { links } from "../../../config"; 14 | 15 | // Response Handler 16 | import ResponseHandler from "../../../util/ResponseHandler"; 17 | 18 | // Declare the Change Password Controller. 19 | const ChangePasswordController = {}; 20 | 21 | /** 22 | * Determine post Change Password response action 23 | * 24 | * Function takes in the status of the Change Password response in order to 25 | * determine the post Change Password action to be taken, whether that is to 26 | * display an error message, or to redirect the user. A promise is used 27 | * to redirect the user while reject is used to display a message. 28 | * 29 | * @param {Number} response The response from the API request. 30 | * @param {Object} languageData Current language information for the app. Language data object. 31 | */ 32 | ChangePasswordController.handleResponse = async (response, languageData) => { 33 | // Redirect data for the redirectable statuses. 34 | const redirectData = { 35 | "200": { 36 | redirect: links.home, 37 | alert: true 38 | } 39 | }; 40 | // Alert data information for statuses. All statuses in this case use 41 | // the `danger` type, so we default all statuses to this with `500` as the key. 42 | const alertData = { 43 | "500": "danger" 44 | } 45 | 46 | // Return a promise with the results. 47 | return new Promise(async (resolve, reject) => { 48 | try { 49 | // Resolve with the redirect link. 50 | resolve(await ResponseHandler.status({ response, apiMessage: true, languageData, langKey: null, redirectData, alertData })); 51 | } catch (error) { 52 | // Reject with the error object. 53 | reject(error); 54 | } 55 | }); 56 | }; 57 | 58 | /** 59 | * Handle Change Password Actions 60 | * 61 | * Uses the custom Response Handler to handle Change Password actions. 62 | * 63 | * @param {Object} responseObject Response object from the request with alert info or redirect info. 64 | * @param {Object} data Data object required for handling the action. 65 | * @param {Function} action Redux action to handle data. 66 | */ 67 | ChangePasswordController.handleAction = (responseObject, data, action) => { 68 | // Add additional content to the response object to be handled. 69 | responseObject = { 70 | ...responseObject, 71 | setUser: true 72 | } 73 | 74 | // Use the custom Response Handler to determine what to do with the user. 75 | const response = ResponseHandler.action(responseObject, data, action); 76 | 77 | // Check if there are any errors to display. 78 | if (get(response, "type")) { 79 | // Return the error information. 80 | return response; 81 | } else { 82 | // No error, so the user was redirected. 83 | return false; 84 | } 85 | } 86 | 87 | // Export the Change Password Controller. 88 | export default ChangePasswordController; -------------------------------------------------------------------------------- /frontend/src/views/Auth/ChangePassword/ChangePasswordForm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputColXL": 8, 4 | "inputColMD": 12, 5 | "inputColClassName": "mx-auto", 6 | "type": "password", 7 | "name": "password", 8 | "prependIcon": "lock", 9 | "validation": { 10 | "required": { 11 | "langKey": [ 12 | "common", 13 | "input", 14 | "password", 15 | "validation", 16 | "required" 17 | ] 18 | }, 19 | "minLength": { 20 | "rule": 8, 21 | "langKey": [ 22 | "common", 23 | "input", 24 | "password", 25 | "validation", 26 | "minLength" 27 | ] 28 | }, 29 | "max": { 30 | "rule": 256, 31 | "langKey": [ 32 | "common", 33 | "input", 34 | "password", 35 | "validation", 36 | "max" 37 | ] 38 | } 39 | } 40 | }, 41 | { 42 | "inputColXL": 8, 43 | "inputColMD": 12, 44 | "inputColClassName": "mx-auto", 45 | "type": "password", 46 | "name": "confirmPassword", 47 | "prependIcon": "lock", 48 | "validation": { 49 | "required": { 50 | "langKey": [ 51 | "common", 52 | "input", 53 | "confirmPassword", 54 | "validation", 55 | "required" 56 | ] 57 | }, 58 | "confirmValue": { 59 | "langKey": [ 60 | "common", 61 | "input", 62 | "confirmPassword", 63 | "validation", 64 | "confirmValue" 65 | ] 66 | } 67 | } 68 | } 69 | ] -------------------------------------------------------------------------------- /frontend/src/views/Auth/ChangePassword/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import ChangePassword from "./ChangePassword"; 3 | 4 | // Export modules 5 | export default ChangePassword; -------------------------------------------------------------------------------- /frontend/src/views/Auth/ForgotPassword/ForgotPasswordAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the Forgot Password API. 2 | export default { 3 | apiService: { 4 | postForgotPassword: { 5 | PATH_SEARCH: "/api/fusionAuth/forgotPassword", 6 | PATH_METHOD: "post" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/Auth/ForgotPassword/ForgotPasswordController.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Forgot Password Controller 3 | * 4 | * Contains functions related to handling Forgot Password responses and actions. 5 | * Enables redirecting the user depending on the response from the FusionAuth 6 | * server. Also provides alert data for the view if needed. 7 | */ 8 | 9 | // Dependencies 10 | import { get } from "lodash"; 11 | 12 | // Config & Application Links 13 | import { links } from "../../../config"; 14 | 15 | // Response Handler 16 | import ResponseHandler from "../../../util/ResponseHandler"; 17 | 18 | // Declare the Forgot Password Controller. 19 | const ForgotPasswordController = {}; 20 | 21 | /** 22 | * Determine post Forgot Password response action 23 | * 24 | * Function takes in the status of the Forgot Password response in order to 25 | * determine the post Forgot Password action to be taken, whether that is to 26 | * display an error message, or to redirect the user. A promise is used 27 | * to redirect the user while reject is used to display a message. 28 | * 29 | * @param {number} response The response from the API request. 30 | * @param {Object} languageData Current language information for the app. Language data object. 31 | */ 32 | ForgotPasswordController.handleResponse = async (response, languageData) => {; 33 | // Redirect data for the redirectable statuses. 34 | const redirectData = { 35 | "200": { 36 | redirect: links.auth.changePassword, 37 | alert: true 38 | } 39 | }; 40 | // Alert data information for statuses. All statuses in this case use 41 | // the `danger` type, so we default all statuses to this with `500` as the key. 42 | const alertData = { 43 | "500": "danger" 44 | } 45 | 46 | // Return a promise with the results. 47 | return new Promise(async (resolve, reject) => { 48 | try { 49 | // Resolve with the redirect link. 50 | resolve(await ResponseHandler.status({ response, apiMessage: true, languageData, langKey: null, redirectData, alertData })); 51 | } catch (error) { 52 | // Reject with the error object. 53 | reject(error); 54 | } 55 | }); 56 | }; 57 | 58 | /** 59 | * Handle Forgot Password Actions 60 | * 61 | * Uses the custom Response Handler to handle Forgot Password actions. 62 | * 63 | * @param {Object} responseObject Response object from the request with alert info or redirect info. 64 | * @param {Object} data Data object required for handling the action. 65 | * @param {Function} action Redux action to handle data. 66 | */ 67 | ForgotPasswordController.handleAction = (responseObject, data, action) => { 68 | // Add additional content to the response object to be handled. 69 | responseObject = { 70 | ...responseObject, 71 | from: "changeForogtPassword" 72 | } 73 | 74 | // Use the custom Response Handler to determine what to do with the user. 75 | const response = ResponseHandler.action(responseObject, data, action); 76 | 77 | // Check if there are any errors to display. 78 | if (get(response, "type")) { 79 | // Return the error information. 80 | return response; 81 | } else { 82 | // No error, so the user was redirected. 83 | return false; 84 | } 85 | } 86 | 87 | // Export the Forgot Password Controller. 88 | export default ForgotPasswordController; -------------------------------------------------------------------------------- /frontend/src/views/Auth/ForgotPassword/ForgotPasswordForm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputColXL": 8, 4 | "inputColMD": 12, 5 | "inputColClassName": "mx-auto", 6 | "type": "test", 7 | "name": "loginId", 8 | "prependIcon": "user", 9 | "validation": { 10 | "required": { 11 | "langKey": [ 12 | "common", 13 | "input", 14 | "loginId", 15 | "validation", 16 | "required" 17 | ] 18 | } 19 | } 20 | } 21 | ] -------------------------------------------------------------------------------- /frontend/src/views/Auth/ForgotPassword/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import ForgotPassword from "./ForgotPassword"; 3 | 4 | // Export modules 5 | export default ForgotPassword; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Login/LoginAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the Login API. 2 | export default { 3 | fusionAuth: { 4 | getLogin: { 5 | PATH_SEARCH: "/api/login", 6 | PATH_METHOD: "post" 7 | } 8 | }, 9 | apiService: { 10 | getLogin: { 11 | PATH_SEARCH: "/api/fusionAuth/login", 12 | PATH_METHOD: "post" 13 | } 14 | } 15 | }; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Login/LoginController.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Login Controller 3 | * 4 | * Contains functions related to login within the application. It handles 5 | * login response from the FusionAuth server and also sets the cookies with 6 | * the API server for subsequent requests that required authentication. 7 | */ 8 | 9 | // Dependencies 10 | import axios from "axios"; 11 | import { get } from "lodash"; 12 | 13 | // Config & Application Links 14 | import { config, links } from "../../../config"; 15 | 16 | // Login API endpoints 17 | import LoginAPI from "./LoginAPI"; 18 | 19 | // Response Handler 20 | import ResponseHandler from "../../../util/ResponseHandler"; 21 | 22 | // Declare the Login Controller. 23 | const LoginController = {}; 24 | 25 | /** 26 | * Determine post login action (from FusionAuth) 27 | * 28 | * Function takes in the status of the login response in order to 29 | * determine the post login action to be taken, whether that is to 30 | * display an error message, or to redirect the user. A promise is used 31 | * to redirect the user while reject is used to display a message. 32 | * 33 | * @param {number} response The response from the API request. 34 | * @param {Object} languageData Current language information for the app. Language data object. 35 | */ 36 | LoginController.handleResponse = async (response, languageData) => { 37 | // Base path for language keys for the statuses. 38 | const langKey = ["common", "auth", "login", "status"]; 39 | // Redirect data for the redirectable statuses. 40 | const redirectData = { 41 | "200": { 42 | redirect: links.home 43 | }, 44 | "203":{ 45 | redirect: links.auth.changePassword, 46 | alert: true 47 | }, 48 | "242": { 49 | redirect: links.auth.twoFactor, 50 | alert: true 51 | } 52 | }; 53 | // Alert data information for statuses. All statuses in this case use 54 | // the `danger` type, so we default all statuses to this with `500` as the key. 55 | const alertData = { 56 | "500": "danger" 57 | } 58 | 59 | // Return a promise with the results. 60 | return new Promise(async (resolve, reject) => { 61 | try { 62 | // Resolve with the redirect link. 63 | resolve(await ResponseHandler.status({ response, apiMessage: false, languageData, langKey, redirectData, alertData })); 64 | } catch (error) { 65 | // Reject with the error object. 66 | reject(error); 67 | } 68 | }); 69 | }; 70 | 71 | /** 72 | * Handle Login Actions 73 | * 74 | * Uses the custom Response Handler to handle login actions. 75 | * 76 | * @param {Object} responseObject Response object from the request with alert info or redirect info. 77 | * @param {Object} data Data object required for handling the action. 78 | * @param {Function} action Redux action to handle data. 79 | */ 80 | LoginController.handleAction = (responseObject, data, action) => { 81 | // Add additional content to the response object to be handled. 82 | responseObject = { 83 | ...responseObject, 84 | setUser: true, 85 | from: "changePassword" 86 | } 87 | 88 | // Use the custom Response Handler to determine what to do with the user. 89 | const response = ResponseHandler.action(responseObject, data, action); 90 | 91 | // Check if there are any errors to display. 92 | if (get(response, "type")) { 93 | // Return the error information. 94 | return response; 95 | } else { 96 | // No error, so the user was redirected. 97 | return false; 98 | } 99 | } 100 | 101 | /** 102 | * Set cookies on successful login 103 | * 104 | * Function makes a request to the backend API service to set cookies 105 | * for future requests in order to allow the user to access the API endpoints 106 | * for the application. 107 | * 108 | * @param {string} refreshToken User refresh token from the login response. 109 | * @param {string} token User access token from the login response. 110 | * @param {String} locale The current locale of the application. 111 | * @param {Object} languageData Current language information for the app. Language data object. 112 | */ 113 | LoginController.setCookies = (refreshToken, token, locale, languageData) => { 114 | // Return a promise with the redirect url or the error response. 115 | return new Promise((resolve, reject) => { 116 | axios({ 117 | baseURL: config.apiServer.BASEURL, 118 | url: LoginAPI.apiService.getLogin.PATH_SEARCH, 119 | method: LoginAPI.apiService.getLogin.PATH_METHOD, 120 | withCredentials: true, 121 | data: { 122 | refreshToken, 123 | token 124 | }, 125 | headers: { 126 | locale 127 | } 128 | }).then(() => { 129 | resolve({ redirect: links.home }) 130 | }).catch(() => reject(get(languageData, ["common", "auth", "failedLogin"]))); 131 | }); 132 | }; 133 | 134 | // Export the Login Controller. 135 | export default LoginController; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Login/LoginForm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputColXL": 8, 4 | "inputColMD": 12, 5 | "inputColClassName": "mx-auto", 6 | "type": "text", 7 | "name": "loginId", 8 | "prependIcon": "user" 9 | }, 10 | { 11 | "inputColXL": 8, 12 | "inputColMD": 12, 13 | "inputColClassName": "mx-auto", 14 | "type": "password", 15 | "name": "password", 16 | "prependIcon": "lock" 17 | } 18 | ] -------------------------------------------------------------------------------- /frontend/src/views/Auth/Login/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Login from "./Login"; 3 | 4 | // Export modules 5 | export default Login; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Register/RegisterAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the Registration API. 2 | export default { 3 | apiService: { 4 | postRegister: { 5 | PATH_SEARCH: "/api/fusionAuth/registration/", 6 | PATH_METHOD: "post" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Register/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Register from "./Register"; 3 | 4 | // Export modules 5 | export default Register; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Verify/Verify2Factor/Verify2FactorAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the Verify 2Factor API. 2 | export default { 3 | apiService: { 4 | post2FAVerfiy: { 5 | PATH_SEARCH: "/api/fusionAuth/verify/2fa", 6 | PATH_METHOD: "post" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Verify/Verify2Factor/Verify2FactorController.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Verify 2Factor Controller 3 | * 4 | * Contains functions related to handling 2Factor responses and actions. 5 | * Enables redirecting the user depending on the response from the API 6 | * services. Also provides alert data for the view if needed. 7 | */ 8 | 9 | // Dependencies 10 | import { get } from "lodash"; 11 | 12 | // Config & Application Links 13 | import { links } from "../../../../config"; 14 | 15 | // Response Handler 16 | import ResponseHandler from "../../../../util/ResponseHandler"; 17 | 18 | // Declare the Verify 2Factor Controller. 19 | const Verify2FactorController = {}; 20 | 21 | /** 22 | * Determine post 2Factor login response action 23 | * 24 | * Function takes in the status of the 2Factor response in order to 25 | * determine the post 2FA action to be taken, whether that is to 26 | * display an error message, or to redirect the user. A promise is used 27 | * to redirect the user while reject is used to display a message. 28 | * 29 | * @param {number} response The response from the API request. 30 | * @param {Object} languageData Current language information for the app. Language data object. 31 | */ 32 | Verify2FactorController.handleResponse = async (response, languageData) => { 33 | // Redirect data for the redirectable statuses. 34 | const redirectData = { 35 | "200": { 36 | redirect: links.home 37 | }, 38 | "203": { 39 | redirect: links.auth.changePassword, 40 | alert: true 41 | } 42 | }; 43 | // Alert data information for statuses. All statuses in this case use 44 | // the `danger` type, so we default all statuses to this with `500` as the key. 45 | const alertData = { 46 | "500": "danger" 47 | } 48 | 49 | // Return a promise with the results. 50 | return new Promise(async (resolve, reject) => { 51 | try { 52 | // Resolve with the redirect link. 53 | resolve(await ResponseHandler.status({ response, apiMessage: true, languageData, langKey: null, redirectData, alertData })); 54 | } catch (error) { 55 | // Reject with the error object. 56 | reject(error); 57 | } 58 | }); 59 | }; 60 | 61 | /** 62 | * Handle 2Factor Login Actions 63 | * 64 | * Uses the custom Response Handler to handle 2Factor actions. 65 | * 66 | * @param {Object} responseObject Response object from the request with alert info or redirect info. 67 | * @param {Object} data Data object required for handling the action. 68 | * @param {Function} action Redux action to handle data. 69 | */ 70 | Verify2FactorController.handleAction = (responseObject, data, action) => { 71 | // Add additional content to the response object to be handled. 72 | responseObject = { 73 | ...responseObject, 74 | setUser: true, 75 | from: "changePassword" 76 | } 77 | 78 | // Use the custom Response Handler to determine what to do with the user. 79 | const response = ResponseHandler.action(responseObject, data, action); 80 | 81 | // Check if there are any errors to display. 82 | if (get(response, "type")) { 83 | // Return the error information. 84 | return response; 85 | } else { 86 | // No error, so the user was redirected. 87 | return false; 88 | } 89 | } 90 | 91 | // Export the Verify 2Factor Controller. 92 | export default Verify2FactorController; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Verify/Verify2Factor/Verify2FactorForm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputColXL": 8, 4 | "inputColMD": 12, 5 | "inputColClassName": "mx-auto", 6 | "type": "text", 7 | "name": "code", 8 | "prependIcon": "shield-alt", 9 | "validation": { 10 | "required": { 11 | "langKey": [ 12 | "common", 13 | "input", 14 | "code", 15 | "validation", 16 | "required" 17 | ] 18 | }, 19 | "length": { 20 | "rule": 6, 21 | "langKey": [ 22 | "common", 23 | "input", 24 | "code", 25 | "validation", 26 | "length" 27 | ] 28 | } 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /frontend/src/views/Auth/Verify/Verify2Factor/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Verify2Factor from "./Verify2Factor"; 3 | 4 | // Export modules 5 | export default Verify2Factor; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Verify/VerifyEmail/VerifyEmailAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the Verify Email API. 2 | export default { 3 | apiService: { 4 | postEmailVerify: { 5 | PATH_SEARCH: "/api/fusionAuth/verify/email/", 6 | PATH_METHOD: "post" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Verify/VerifyEmail/VerifyEmailForm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputColXL": 8, 4 | "inputColMD": 12, 5 | "inputColClassName": "mx-auto", 6 | "type": "text", 7 | "name": "verificationId", 8 | "prependIcon": "code", 9 | "validation": { 10 | "required": { 11 | "langKey": [ 12 | "common", 13 | "input", 14 | "verificationId", 15 | "validation", 16 | "required" 17 | ] 18 | }, 19 | "length": { 20 | "rule": 43, 21 | "langKey": [ 22 | "common", 23 | "input", 24 | "verificationId", 25 | "validation", 26 | "length" 27 | ] 28 | } 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /frontend/src/views/Auth/Verify/VerifyEmail/VerifyEmailPageData.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | 4 | // Components 5 | import PageData from "../../../../components/Util/PageData"; 6 | 7 | /** 8 | * Verify Email Page Data Component 9 | * 10 | * Display the proper components for the user if the verification ID was 11 | * supplied to the page via the URL. 12 | * 13 | * @param {Boolean} isLoading Loading indication for API request. 14 | * @param {Object} hasError Error object from the API request. 15 | */ 16 | const VerifyEmailPageData = ({ isLoading, hasError }) => { 17 | // Return the formatted result, with error and loading indication. 18 | return ; 22 | } 23 | 24 | // Export the Verify Email Page Data Component. 25 | export default VerifyEmailPageData; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Verify/VerifyEmail/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import VerifyEmail from "./VerifyEmail"; 3 | 4 | // Export modules 5 | export default VerifyEmail; -------------------------------------------------------------------------------- /frontend/src/views/Auth/Verify/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import VerifyEmail from "./VerifyEmail"; 3 | import Verify2Factor from "./Verify2Factor"; 4 | 5 | // Export modules 6 | export { 7 | VerifyEmail, 8 | Verify2Factor 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/Home.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { get } from "lodash"; 4 | import { connect } from "react-redux"; 5 | import { 6 | Card, 7 | CardHeader, 8 | CardBody, 9 | Container, 10 | Row, 11 | Col 12 | } from "reactstrap"; 13 | 14 | /** 15 | * Home View 16 | * 17 | * Display some information on the home page about the NodeJS + React + FusionAuth 18 | * example application. 19 | * 20 | * @param {Object} languageData Current language information for the app. Language data object. 21 | */ 22 | const Home = ({ languageData }) => ( 23 | 24 | 25 | 26 | 27 | 28 | { get(languageData, ["common", "about"]) } 29 | 30 | 31 | { get(languageData, ["common", "aboutDesc"]) } 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | 39 | /** 40 | * Get App State 41 | * 42 | * Get the requried state for the component from the Redux store. 43 | * 44 | * @param {Object} state Application state from Redux. 45 | */ 46 | const mapStateToProps = state => { 47 | return { 48 | languageData: state.language.languageData 49 | } 50 | } 51 | 52 | // Export the Home View. 53 | export default connect(mapStateToProps)(Home); 54 | -------------------------------------------------------------------------------- /frontend/src/views/NotFound/NotFound.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { get } from "lodash"; 4 | import { connect } from "react-redux"; 5 | import { 6 | Card, 7 | CardHeader, 8 | CardBody, 9 | Container, 10 | Row, 11 | Col 12 | } from "reactstrap"; 13 | 14 | /** 15 | * Not Found View 16 | * 17 | * For all not found requests, display a 404 Not Found message. 18 | * 19 | * @param {Object} languageData Current language information for the app. Language data object. 20 | */ 21 | const NotFound = ({ languageData }) => ( 22 | 23 | 24 | 25 | 26 | 27 | { get(languageData, ["common", "404", "title"]) } 28 | 29 | 30 | { get(languageData, ["common", "404", "message"]) } 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | 38 | /** 39 | * Get App State 40 | * 41 | * Get the requried state for the component from the Redux store. 42 | * 43 | * @param {Object} state Application state from Redux. 44 | */ 45 | const mapStateToProps = state => { 46 | return { 47 | languageData: state.language.languageData 48 | } 49 | } 50 | 51 | // Export the Not Found View. 52 | export default connect(mapStateToProps)(NotFound); -------------------------------------------------------------------------------- /frontend/src/views/NotFound/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import NotFound from "./NotFound"; 3 | 4 | // Export modules 5 | export default NotFound; -------------------------------------------------------------------------------- /frontend/src/views/Todo/TodoAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the ToDo API. 2 | export default { 3 | apiService: { 4 | getTodos: { 5 | PATH_SEARCH: "/api/todo", 6 | PATH_METHOD: "get" 7 | }, 8 | addTodo: { 9 | PATH_SEARCH: "/api/todo", 10 | PATH_METHOD: "post" 11 | }, 12 | editTodo: { 13 | PATH_SEARCH: "/api/todo", 14 | PATH_METHOD: "put" 15 | }, 16 | getTodo: { 17 | PATH_SEARCH: "/api/todo/one", 18 | PATH_METHOD: "get" 19 | }, 20 | deleteTodo: { 21 | PATH_SEARCH: "/api/todo", 22 | PATH_METHOD: "delete" 23 | } 24 | } 25 | }; -------------------------------------------------------------------------------- /frontend/src/views/Todo/TodoList/TodoList.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { get } from "lodash"; 4 | import { connect } from "react-redux"; 5 | import { Link } from "react-router-dom"; 6 | import classNames from "classnames"; 7 | import { 8 | Card, 9 | CardHeader, 10 | CardBody, 11 | Container, 12 | Row, 13 | Col 14 | } from "reactstrap"; 15 | 16 | // Config 17 | import { config, links } from "../../../config"; 18 | 19 | // API 20 | import APIFetch from "../../../util/APIFetch"; 21 | import TodoAPI from "../TodoAPI"; 22 | 23 | // Components 24 | import CustomButton from "../../../components/CustomButton"; 25 | 26 | // Page Data 27 | import TodoListPageData from "./TodoListPageData"; 28 | 29 | /** 30 | * Todo List View 31 | * 32 | * The Todo view contains information for all of the user's ToDo items. From here, 33 | * they are able to view the individual Todo items. 34 | * 35 | * @param {String} locale The current locale of the application. 36 | * @param {Object} languageData Current language information for the app. Language data object. 37 | */ 38 | const TodoList = ({ locale, languageData }) => { 39 | // Setup the API Fetch utility for the Todo List View. 40 | const [{ isLoading, results, hasError }] = APIFetch({ 41 | locale, 42 | BASEURL: config.apiServer.BASEURL, 43 | PATH_SEARCH: TodoAPI.apiService.getTodos.PATH_SEARCH, 44 | PATH_METHOD: TodoAPI.apiService.getTodos.PATH_METHOD 45 | }); 46 | 47 | // Create a dynamic class name for the panel that will center the text 48 | // if the result status is not 200. 49 | const isCenteredText = classNames({ 50 | "mx-auto": isLoading || get(results, "status") !== 200 51 | }); 52 | 53 | // Render the Todo List View. 54 | return ( 55 | 56 | 57 | 58 | 59 | 60 | { get(languageData, ["common", "todo", "list", "title"]) } 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | } 82 | 83 | /** 84 | * Get App State 85 | * 86 | * Get the requried state for the component from the Redux store. 87 | * 88 | * @param {Object} state Application state from Redux. 89 | */ 90 | const mapStateToProps = state => { 91 | return { 92 | locale: state.language.locale, 93 | languageData: state.language.languageData 94 | } 95 | } 96 | 97 | // Export the Todo List View. 98 | export default connect(mapStateToProps)(TodoList); 99 | -------------------------------------------------------------------------------- /frontend/src/views/Todo/TodoList/TodoListPageData.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { Link } from "react-router-dom"; 4 | import { get, map } from "lodash"; 5 | import { Col } from "reactstrap"; 6 | 7 | // Components 8 | import PageData from "../../../components/Util/PageData"; 9 | 10 | // Links 11 | import { links } from "../../../config"; 12 | 13 | /** 14 | * ToDo List Page Data Component 15 | * 16 | * Display the user's ToDo list properly. 17 | * 18 | * @param {Boolean} isLoading Loading indication for API request. 19 | * @param {Object} results Result object from the API request. 20 | * @param {Object} hasError Error object from the API request. 21 | * @param {Object} languageData Current language information for the app. Language data object. 22 | */ 23 | const TodoPageData = ({ isLoading, results, hasError, languageData }) => { 24 | // ToDo status indicators. 25 | const todoStatus = { 26 | true: { get(languageData, ["common", "done"]) }, 27 | false: { get(languageData, ["common", "incomplete"]) } 28 | } 29 | 30 | // Display ToDo items for the current user if there are any. Otherwise, return nothing. 31 | // The default text is handled by the API service. 32 | const todoInfo = get(results, "data") 33 | ? map(results.data.content, (result, key) => ( 34 | 35 | { todoStatus[result.done] }  36 | 37 | 38 | { result.name } 39 | 40 | 41 |
    42 | { result.description } 43 | 44 | )) 45 | : null; 46 | 47 | // Return the formatted result, with error and loading indication. 48 | return ; 54 | } 55 | 56 | // Export the Todo List Page Data Component. 57 | export default TodoPageData; -------------------------------------------------------------------------------- /frontend/src/views/Todo/TodoList/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import TodoList from "./TodoList"; 3 | 4 | // Export modules 5 | export default TodoList; -------------------------------------------------------------------------------- /frontend/src/views/Todo/TodoModify/TodoModifyForm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputColMD": 12, 4 | "inputColClassName": "mx-auto", 5 | "id": "done", 6 | "type": "checkbox", 7 | "name": "done", 8 | "formGroupClassName": "custom-control custom-control-alternative custom-checkbox", 9 | "labelClassName": "custom-control-label", 10 | "inputClassName": "custom-control-input" 11 | }, 12 | { 13 | "inputColMD": 12, 14 | "inputColClassName": "mx-auto", 15 | "type": "text", 16 | "name": "name", 17 | "prependIcon": "tasks", 18 | "validation": { 19 | "required": { 20 | "langKey": [ 21 | "common", 22 | "input", 23 | "name", 24 | "validation", 25 | "required" 26 | ] 27 | }, 28 | "minLength": { 29 | "rule": 5, 30 | "langKey": [ 31 | "common", 32 | "input", 33 | "name", 34 | "validation", 35 | "minLength" 36 | ] 37 | }, 38 | "max": { 39 | "rule": 150, 40 | "langKey": [ 41 | "common", 42 | "input", 43 | "name", 44 | "validation", 45 | "max" 46 | ] 47 | } 48 | } 49 | }, 50 | { 51 | "inputColMD": 12, 52 | "inputColClassName": "mx-auto", 53 | "type": "textarea", 54 | "name": "description", 55 | "prependIcon": "feather-alt", 56 | "rows": 5, 57 | "validation": { 58 | "required": { 59 | "langKey": [ 60 | "common", 61 | "input", 62 | "description", 63 | "validation", 64 | "required" 65 | ] 66 | }, 67 | "minLength": { 68 | "rule": 10, 69 | "langKey": [ 70 | "common", 71 | "input", 72 | "description", 73 | "validation", 74 | "minLength" 75 | ] 76 | } 77 | } 78 | } 79 | ] -------------------------------------------------------------------------------- /frontend/src/views/Todo/TodoModify/TodoModifyPageData.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | 4 | // Components 5 | import PageData from "../../../components/Util/PageData"; 6 | 7 | /** 8 | * ToDo Item Data Component 9 | * 10 | * Display the ToDo item of a given ID properly. 11 | * 12 | * @param {Boolean} isLoading Loading indication for API request. 13 | * @param {Object} results Result object from the API request. 14 | * @param {Object} hasError Error object from the API request. 15 | * @param {Function} component The component that needs to be diplayed, as a function. 16 | */ 17 | const TodoPageData = ({ isLoading, results, hasError, component }) => { 18 | // Display the ToDo item for the current user with the given ID if it exists. 19 | const todoInfo = component(); 20 | 21 | // Return the formatted result, with error and loading indication. 22 | return ; 28 | } 29 | 30 | // Export the ToDo Item Data Component. 31 | export default TodoPageData; -------------------------------------------------------------------------------- /frontend/src/views/Todo/TodoModify/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import TodoModify from "./TodoModify"; 3 | 4 | // Export modules 5 | export default TodoModify; -------------------------------------------------------------------------------- /frontend/src/views/Todo/TodoView/TodoView.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React, { useEffect } from "react"; 3 | import { get } from "lodash"; 4 | import { connect } from "react-redux"; 5 | import { Link } from "react-router-dom"; 6 | import classNames from "classnames"; 7 | import { 8 | Card, 9 | CardHeader, 10 | CardBody, 11 | Container, 12 | Row, 13 | Col 14 | } from "reactstrap"; 15 | 16 | // Config 17 | import { config, links } from "../../../config"; 18 | 19 | // API 20 | import APIFetch from "../../../util/APIFetch"; 21 | import TodoAPI from "../TodoAPI"; 22 | 23 | // Components 24 | import CustomButton from "../../../components/CustomButton"; 25 | 26 | // Page Data 27 | import TodoViewPageData from "./TodoViewPageData"; 28 | 29 | // History 30 | import History from "../../../util/History"; 31 | 32 | // Toasty 33 | import Toasty from "../../../util/Toasty"; 34 | 35 | /** 36 | * Todo Item View 37 | * 38 | * The Todo view contains information for all of the user's Todo list items. From here, 39 | * they are able to edit, view, and delete the individual Todo items. 40 | * 41 | * @param {Object} match Information about the view's route. 42 | * @param {String} locale The current locale of the application. 43 | * @param {Object} languageData Current language information for the app. Language data object. 44 | */ 45 | const TodoView = ({ match, locale, languageData }) => { 46 | // Setup the API Fetch utility for the Todo View. 47 | const [{ isLoading, results, hasError }] = APIFetch({ 48 | locale, 49 | BASEURL: config.apiServer.BASEURL, 50 | PATH_SEARCH: TodoAPI.apiService.getTodo.PATH_SEARCH, 51 | PATH_METHOD: TodoAPI.apiService.getTodo.PATH_METHOD, 52 | PATH_QUERY: `?id=${ match.params.id }` 53 | }); 54 | 55 | // Create a dynamic class name for the panel that will center the text 56 | // if the result status is not 200. 57 | const isCenteredText = classNames({ 58 | "mx-auto": isLoading || get(results, "status") !== 200 59 | }); 60 | 61 | /** 62 | * Listen to changes in the error state. 63 | */ 64 | useEffect(() => { 65 | const handleError = () => { 66 | // Check the error results and direct the user accordingly. 67 | if (get(hasError, "status") === 410) { 68 | // Let the user know the item does not exist. 69 | Toasty.notify({ 70 | type: Toasty.error(), 71 | content: hasError.data.message 72 | }); 73 | 74 | // Redirect the user to the proper page. 75 | History.push(links.todo.list); 76 | } 77 | } 78 | 79 | handleError(); 80 | }, [hasError]); 81 | 82 | // Render the Todo Item View. 83 | return ( 84 | 85 | 86 | 87 | 88 | 89 | { get(languageData, ["common", "todo", "view", "title"]) } 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 104 | 105 | 106 | 107 | 108 | 109 | ); 110 | } 111 | 112 | /** 113 | * Get App State 114 | * 115 | * Get the requried state for the component from the Redux store. 116 | * 117 | * @param {Object} state Application state from Redux. 118 | */ 119 | const mapStateToProps = state => { 120 | return { 121 | locale: state.language.locale, 122 | languageData: state.language.languageData 123 | } 124 | } 125 | 126 | // Export the Todo Item View. 127 | export default connect(mapStateToProps)(TodoView); 128 | -------------------------------------------------------------------------------- /frontend/src/views/Todo/TodoView/TodoViewPageData.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { get, map } from "lodash"; 4 | import { Col } from "reactstrap"; 5 | import { Link } from "react-router-dom"; 6 | 7 | // Components 8 | import PageData from "../../../components/Util/PageData"; 9 | import CustomButton from "../../../components/CustomButton"; 10 | 11 | // Config 12 | import { links } from "../../../config"; 13 | 14 | /** 15 | * ToDo Item Data Component 16 | * 17 | * Display the ToDo item of a given ID properly. 18 | * 19 | * @param {Boolean} isLoading Loading indication for API request. 20 | * @param {Object} results Result object from the API request. 21 | * @param {Object} hasError Error object from the API request. 22 | * @param {Object} languageData Current language information for the app. Language data object. 23 | */ 24 | const TodoPageData = ({ isLoading, results, hasError, languageData }) => { 25 | // Names of the field we want to display and the value of the header for the field. 26 | const fieldNames = { 27 | _id: get(languageData, ["common", "todo", "fields", "_id"]), 28 | name: get(languageData, ["common", "todo", "fields", "name"]), 29 | description: get(languageData, ["common", "todo", "fields", "description"]), 30 | done: get(languageData, ["common", "todo", "fields", "done"]), 31 | updatedAt: get(languageData, ["common", "updatedAt"]), 32 | createdAt: get(languageData, ["common", "createdAt"]) 33 | } 34 | 35 | // ToDo status indicators. 36 | const todoStatus = { 37 | true: { get(languageData, ["common", "done"]) }, 38 | false: { get(languageData, ["common", "incomplete"]) } 39 | } 40 | 41 | // Display the ToDo item for the current user with the given ID if it exists. 42 | const todoInfo = get(results, ["data", "content"]) 43 | ? <> 44 | { map(results.data.content, (result, key) => ( 45 | fieldNames.hasOwnProperty(key) && 46 | 47 | { fieldNames[key] }
    48 | { key === "done" ? todoStatus[result] : result } 49 | 50 | ))} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | : null; 59 | 60 | // Return the formatted result, with error and loading indication. 61 | return ; 67 | } 68 | 69 | // Export the ToDo Item Data Component. 70 | export default TodoPageData; -------------------------------------------------------------------------------- /frontend/src/views/Todo/TodoView/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import TodoView from "./TodoView"; 3 | 4 | // Export modules 5 | export default TodoView; -------------------------------------------------------------------------------- /frontend/src/views/Todo/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import TodoList from "./TodoList"; 3 | import TodoModify from "./TodoModify"; 4 | import TodoView from "./TodoView"; 5 | 6 | // Export modules 7 | export { 8 | TodoList, 9 | TodoModify, 10 | TodoView 11 | }; -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/Disable/Disable2FAAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the User 2Factor Disable API. 2 | export default { 3 | apiService: { 4 | postDisable2FA: { 5 | PATH_SEARCH: "/api/user/2fa", 6 | PATH_METHOD: "delete" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/Disable/Disable2FAController.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * User 2Factor Disable Controller 3 | * 4 | * Contains functions related to handling 2FA Disable responses and actions. 5 | * Enables redirecting the user depending on the response from the API 6 | * service. Also provides alert data for the view if needed. 7 | */ 8 | 9 | // Dependencies 10 | import { get } from "lodash"; 11 | 12 | // Config & Application Links 13 | import { links } from "../../../../config"; 14 | 15 | // Response Handler 16 | import ResponseHandler from "../../../../util/ResponseHandler"; 17 | 18 | // Declare the User 2Factor Disable Controller. 19 | const Disable2FactorController = {}; 20 | 21 | /** 22 | * Determine post User 2Factor Disable response action 23 | * 24 | * Function takes in the status of the User 2Factor Disable response in order to 25 | * determine the post 2FA action to be taken, whether that is to 26 | * display an error message, or to redirect the user. A promise is used 27 | * to redirect the user while reject is used to display a message. 28 | * 29 | * @param {number} response The response from the API request. 30 | * @param {Object} languageData Current language information for the app. Language data object. 31 | */ 32 | Disable2FactorController.handleResponse = async (response, languageData) => { 33 | // Redirect data for the redirectable statuses. 34 | const redirectData = { 35 | "200": { 36 | redirect: links.user.profile, 37 | alert: true 38 | } 39 | }; 40 | // Alert data information for statuses. All statuses in this case use 41 | // the `danger` type, so we default all statuses to this with `500` as the key. 42 | const alertData = { 43 | "500": "danger" 44 | } 45 | 46 | // Return a promise with the results. 47 | return new Promise(async (resolve, reject) => { 48 | try { 49 | // Resolve with the redirect link. 50 | resolve(await ResponseHandler.status({ response, apiMessage: true, languageData, langKey: null, redirectData, alertData })); 51 | } catch (error) { 52 | // Reject with the error object. 53 | reject(error); 54 | } 55 | }); 56 | }; 57 | 58 | /** 59 | * Handle User 2Factor Disable Actions 60 | * 61 | * Uses the custom Response Handler to handle User 2Factor Disable actions. 62 | * 63 | * @param {Object} responseObject Response object from the request with alert info or redirect info. 64 | * @param {Object} data Data object required for handling the action. 65 | * @param {Function} action Redux action to handle data. 66 | */ 67 | Disable2FactorController.handleAction = (responseObject, data, action) => { 68 | // Use the custom Response Handler to determine what to do with the user. 69 | const response = ResponseHandler.action(responseObject, data, action); 70 | 71 | // Check if there are any errors to display. 72 | if (get(response, "type")) { 73 | // Return the error information. 74 | return response; 75 | } else { 76 | // No error, so the user was redirected. 77 | return false; 78 | } 79 | } 80 | 81 | // Export the Verify 2Factor Controller. 82 | export default Disable2FactorController; -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/Disable/Disable2FAForm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputColXL": 8, 4 | "inputColMD": 12, 5 | "inputColClassName": "mx-auto", 6 | "type": "text", 7 | "name": "code", 8 | "prependIcon": "shield-alt", 9 | "validation": { 10 | "required": { 11 | "langKey": [ 12 | "common", 13 | "input", 14 | "code", 15 | "validation", 16 | "required" 17 | ] 18 | }, 19 | "length": { 20 | "rule": 6, 21 | "langKey": [ 22 | "common", 23 | "input", 24 | "code", 25 | "validation", 26 | "length" 27 | ] 28 | } 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/Disable/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Disable from "./Disable"; 3 | 4 | // Export modules 5 | export default Disable; -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/Enable/Enable2FAAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the User 2Factor Enable API. 2 | export default { 3 | apiService: { 4 | postEnable2FA: { 5 | PATH_SEARCH: "/api/user/2fa", 6 | PATH_METHOD: "post" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/Enable/Enable2FAController.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * User 2Factor Enable Controller 3 | * 4 | * Contains functions related to handling 2FA Enable responses and actions. 5 | * Enables redirecting the user depending on the response from the API 6 | * service. Also provides alert data for the view if needed. 7 | */ 8 | 9 | // Dependencies 10 | import { get } from "lodash"; 11 | 12 | // Config & Application Links 13 | import { links } from "../../../../config"; 14 | 15 | // Response Handler 16 | import ResponseHandler from "../../../../util/ResponseHandler"; 17 | 18 | // Declare the User 2Factor Enable Controller. 19 | const Enable2FactorController = {}; 20 | 21 | /** 22 | * Determine post User 2Factor Enable response action 23 | * 24 | * Function takes in the status of the User 2Factor Enable response in order to 25 | * determine the post 2FA action to be taken, whether that is to 26 | * display an error message, or to redirect the user. A promise is used 27 | * to redirect the user while reject is used to display a message. 28 | * 29 | * @param {number} response The response from the API request. 30 | * @param {Object} languageData Current language information for the app. Language data object. 31 | */ 32 | Enable2FactorController.handleResponse = async (response, languageData) => { 33 | // Redirect data for the redirectable statuses. 34 | const redirectData = { 35 | "200": { 36 | redirect: links.user.profile, 37 | alert: true 38 | } 39 | }; 40 | // Alert data information for statuses. All statuses in this case use 41 | // the `danger` type, so we default all statuses to this with `500` as the key. 42 | const alertData = { 43 | "500": "danger" 44 | } 45 | 46 | // Return a promise with the results. 47 | return new Promise(async (resolve, reject) => { 48 | try { 49 | // Resolve with the redirect link. 50 | resolve(await ResponseHandler.status({ response, apiMessage: true, languageData, langKey: null, redirectData, alertData })); 51 | } catch (error) { 52 | // Reject with the error object. 53 | reject(error); 54 | } 55 | }); 56 | }; 57 | 58 | /** 59 | * Handle User 2Factor Enable Actions 60 | * 61 | * Uses the custom Response Handler to handle User 2Factor Enable actions. 62 | * 63 | * @param {Object} responseObject Response object from the request with alert info or redirect info. 64 | * @param {Object} data Data object required for handling the action. 65 | * @param {Function} action Redux action to handle data. 66 | */ 67 | Enable2FactorController.handleAction = (responseObject, data, action) => { 68 | // Use the custom Response Handler to determine what to do with the user. 69 | const response = ResponseHandler.action(responseObject, data, action); 70 | 71 | // Check if there are any errors to display. 72 | if (get(response, "type")) { 73 | // Return the error information. 74 | return response; 75 | } else { 76 | // No error, so the user was redirected. 77 | return false; 78 | } 79 | } 80 | 81 | // Export the Verify 2Factor Controller. 82 | export default Enable2FactorController; -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/Enable/Enable2FAForm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputColXL": 8, 4 | "inputColMD": 12, 5 | "inputColClassName": "mx-auto", 6 | "type": "text", 7 | "name": "code", 8 | "prependIcon": "shield-alt", 9 | "validation": { 10 | "required": { 11 | "langKey": [ 12 | "common", 13 | "user", 14 | "enable2Factor", 15 | "code", 16 | "validation", 17 | "required" 18 | ] 19 | }, 20 | "length": { 21 | "rule": 6, 22 | "langKey": [ 23 | "common", 24 | "user", 25 | "enable2Factor", 26 | "code", 27 | "validation", 28 | "length" 29 | ] 30 | } 31 | } 32 | } 33 | ] -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/Enable/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import Enable from "./Enable"; 3 | 4 | // Export modules 5 | export default Enable; -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/User2Factor.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { connect } from "react-redux"; 4 | import { get } from "lodash"; 5 | import classNames from "classnames"; 6 | import { 7 | Card, 8 | CardHeader, 9 | CardBody, 10 | Container, 11 | Row, 12 | Col 13 | } from "reactstrap"; 14 | 15 | // Components 16 | import PageData from "../../../components/Util/PageData"; 17 | 18 | // Views 19 | import Enable from "./Enable"; 20 | import Disable from "./Disable"; 21 | 22 | // Config 23 | import { config } from "../../../config"; 24 | 25 | // API 26 | import APIFetch from "../../../util/APIFetch"; 27 | import User2FactorAPI from "./User2FactorAPI"; 28 | 29 | /** 30 | * User 2Factor Settings View 31 | * 32 | * A container view for the Enable/Disable 2Factor pages for logged in users. 33 | * 34 | * @param {String} locale The current locale of the application. 35 | * @param {Object} languageData Current language information for the app. Language data object. 36 | */ 37 | const User2Factor = ({ locale, languageData }) => { 38 | // Setup the API Fetch utility for the User 2Factor Settings View. 39 | const [{ isLoading, results, hasError }] = APIFetch({ 40 | locale, 41 | BASEURL: config.apiServer.BASEURL, 42 | PATH_SEARCH: User2FactorAPI.apiService.get2FA.PATH_SEARCH, 43 | PATH_METHOD: User2FactorAPI.apiService.get2FA.PATH_METHOD 44 | }); 45 | 46 | // Create a dynamic class name for the panel that will center the text 47 | // if the result status is not 200. 48 | const isCenteredText = classNames({ 49 | "mx-auto": isLoading || get(results, "status") !== 200 50 | }); 51 | 52 | // Render the User 2Factor Settings View. 53 | return ( 54 | 55 | 56 | 57 | 58 | 59 | { get(results, ["data", "twoFA"]) === true 60 | ? get(languageData, ["common", "twoFactorDisable"]) 61 | : get(languageData, ["common", "twoFactorEnable"]) 62 | } 63 | 64 | 65 | 66 | { get(results, ["data"]) 67 | ? get(results, ["data", "twoFA"]) === true 68 | ? 69 | : 72 | : 76 | } 77 | 78 | 79 | 80 | 81 | 82 | 83 | ); 84 | } 85 | 86 | /** 87 | * Get App State 88 | * 89 | * Get the requried state for the component from the Redux store. 90 | * 91 | * @param {Object} state Application state from Redux. 92 | */ 93 | const mapStateToProps = state => { 94 | return { 95 | locale: state.language.locale, 96 | languageData: state.language.languageData 97 | } 98 | } 99 | 100 | // Export the User 2Factor Settings View. 101 | export default connect(mapStateToProps)(User2Factor); 102 | -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/User2FactorAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the User 2Factor API. 2 | export default { 3 | apiService: { 4 | get2FA: { 5 | PATH_SEARCH: "/api/user/2fa", 6 | PATH_METHOD: "get" 7 | }, 8 | post2FA: { 9 | PATH_SEARCH: "/api/user/2fa", 10 | PATH_METHOD: "post" 11 | } 12 | } 13 | }; -------------------------------------------------------------------------------- /frontend/src/views/User/User2Factor/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import User2Factor from "./User2Factor"; 3 | 4 | // Export modules 5 | export default User2Factor; -------------------------------------------------------------------------------- /frontend/src/views/User/UserChangePassword/UserChangePasswordAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the User Change Password API. 2 | export default { 3 | apiService: { 4 | postChangePassword: { 5 | PATH_SEARCH: "/api/user/changePassword", 6 | PATH_METHOD: "post" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/User/UserChangePassword/UserChangePasswordController.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * User Change Password Controller 3 | * 4 | * Contains functions related to handling User Change Password responses and actions. 5 | * Enables redirecting the user depending on the response from the API 6 | * service. Also provides alert data for the view if needed. This is for logged in users. 7 | */ 8 | 9 | // Dependencies 10 | import { get } from "lodash"; 11 | 12 | // Config & Application Links 13 | import { links } from "../../../config"; 14 | 15 | // Response Handler 16 | import ResponseHandler from "../../../util/ResponseHandler"; 17 | 18 | // Declare the User Change Password Controller. 19 | const UserChangePasswordController = {}; 20 | 21 | /** 22 | * Determine post User Change Password response action 23 | * 24 | * Function takes in the status of the User Change Password response in order to 25 | * determine the post User Change Password action to be taken, whether that is to 26 | * display an error message, or to redirect the user. A promise is used 27 | * to redirect the user while reject is used to display a message. 28 | * 29 | * @param {number} response The response from the API request. 30 | * @param {Object} languageData Current language information for the app. Language data object. 31 | */ 32 | UserChangePasswordController.handleResponse = async (response, languageData) => { 33 | // Redirect data for the redirectable statuses. 34 | const redirectData = { 35 | "200": { 36 | redirect: links.home, 37 | alert: true 38 | } 39 | }; 40 | // Alert data information for statuses. All statuses in this case use 41 | // the `danger` type, so we default all statuses to this with `500` as the key. 42 | const alertData = { 43 | "500": "danger" 44 | } 45 | 46 | // Return a promise with the results. 47 | return new Promise(async (resolve, reject) => { 48 | try { 49 | // Resolve with the redirect link. 50 | resolve(await ResponseHandler.status({ response, apiMessage: true, languageData, langKey: null, redirectData, alertData })); 51 | } catch (error) { 52 | // Reject with the error object. 53 | reject(error); 54 | } 55 | }); 56 | }; 57 | 58 | /** 59 | * Handle User Change Password Actions 60 | * 61 | * Uses the custom Response Handler to handle User Change Password actions. 62 | * 63 | * @param {Object} responseObject Response object from the request with alert info or redirect info. 64 | * @param {Object} data Data object required for handling the action. 65 | * @param {Function} action Redux action to handle data. 66 | */ 67 | UserChangePasswordController.handleAction = (responseObject, data, action) => { 68 | // Use the custom Response Handler to determine what to do with the user. 69 | const response = ResponseHandler.action(responseObject, data, action); 70 | 71 | // Check if there are any errors to display. 72 | if (get(response, "type")) { 73 | // Return the error information. 74 | return response; 75 | } else { 76 | // No error, so the user was redirected. 77 | return false; 78 | } 79 | } 80 | 81 | // Export the Change Password Controller. 82 | export default UserChangePasswordController; -------------------------------------------------------------------------------- /frontend/src/views/User/UserChangePassword/UserChangePasswordForm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputColXL": 8, 4 | "inputColMD": 12, 5 | "inputColClassName": "mx-auto", 6 | "type": "password", 7 | "name": "currentPassword", 8 | "prependIcon": "user-lock", 9 | "validation": { 10 | "required": { 11 | "langKey": [ 12 | "common", 13 | "input", 14 | "currentPassword", 15 | "validation", 16 | "required" 17 | ] 18 | } 19 | } 20 | }, 21 | { 22 | "inputColXL": 8, 23 | "inputColMD": 12, 24 | "inputColClassName": "mx-auto", 25 | "type": "password", 26 | "name": "password", 27 | "prependIcon": "lock", 28 | "validation": { 29 | "required": { 30 | "langKey": [ 31 | "common", 32 | "input", 33 | "password", 34 | "validation", 35 | "required" 36 | ] 37 | }, 38 | "minLength": { 39 | "rule": 8, 40 | "langKey": [ 41 | "common", 42 | "input", 43 | "password", 44 | "validation", 45 | "minLength" 46 | ] 47 | }, 48 | "max": { 49 | "rule": 256, 50 | "langKey": [ 51 | "common", 52 | "input", 53 | "password", 54 | "validation", 55 | "max" 56 | ] 57 | } 58 | } 59 | }, 60 | { 61 | "inputColXL": 8, 62 | "inputColMD": 12, 63 | "inputColClassName": "mx-auto", 64 | "type": "password", 65 | "name": "confirmPassword", 66 | "prependIcon": "lock", 67 | "validation": { 68 | "required": { 69 | "langKey": [ 70 | "common", 71 | "input", 72 | "confirmPassword", 73 | "validation", 74 | "required" 75 | ] 76 | }, 77 | "confirmValue": { 78 | "langKey": [ 79 | "common", 80 | "input", 81 | "confirmPassword", 82 | "validation", 83 | "confirmValue" 84 | ] 85 | } 86 | } 87 | } 88 | ] -------------------------------------------------------------------------------- /frontend/src/views/User/UserChangePassword/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import UserChangePassword from "./UserChangePassword"; 3 | 4 | // Export modules 5 | export default UserChangePassword; -------------------------------------------------------------------------------- /frontend/src/views/User/UserProfile/UserProfileEdit/UserProfileEditAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the User Profile Edit API. 2 | export default { 3 | apiService: { 4 | getProfile: { 5 | PATH_SEARCH: "/api/user/profile", 6 | PATH_METHOD: "get" 7 | }, 8 | putProfile: { 9 | PATH_SEARCH: "/api/user/profile", 10 | PATH_METHOD: "put" 11 | } 12 | } 13 | }; -------------------------------------------------------------------------------- /frontend/src/views/User/UserProfile/UserProfileEdit/UserProfileEditController.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * User Profile Edit Controller 3 | * 4 | * Contains functions related to handling User Profile Edit responses and actions. 5 | * Enables redirecting the user depending on the response from the API 6 | * service. Also provides alert data for the view if needed. 7 | */ 8 | 9 | // Dependencies 10 | import { get } from "lodash"; 11 | 12 | // Config & Application Links 13 | import { links } from "../../../../config"; 14 | 15 | // Response Handler 16 | import ResponseHandler from "../../../../util/ResponseHandler"; 17 | 18 | // Declare the User Profile Edit Controller. 19 | const UserProfileEditController = {}; 20 | 21 | /** 22 | * Determine post User Profile Edit response action 23 | * 24 | * Function takes in the status of the User Profile Edit response in order to 25 | * determine the post profile save action to be taken, whether that is to 26 | * display an error message, or to redirect the user. A promise is used 27 | * to redirect the user while reject is used to display a message. 28 | * 29 | * @param {number} response The response from the API request. 30 | * @param {Object} languageData Current language information for the app. Language data object. 31 | */ 32 | UserProfileEditController.handleResponse = async (response, languageData) => { 33 | // Redirect data for the redirectable statuses. 34 | const redirectData = { 35 | "200": { 36 | redirect: links.user.profile, 37 | alert: true 38 | } 39 | }; 40 | // Alert data information for statuses. All statuses in this case use 41 | // the `danger` type, so we default all statuses to this with `500` as the key. 42 | const alertData = { 43 | "500": "danger" 44 | } 45 | 46 | // Return a promise with the results. 47 | return new Promise(async (resolve, reject) => { 48 | try { 49 | // Resolve with the redirect link. 50 | resolve(await ResponseHandler.status({ response, apiMessage: true, languageData, langKey: null, redirectData, alertData })); 51 | } catch (error) { 52 | // Reject with the error object. 53 | reject(error); 54 | } 55 | }); 56 | }; 57 | 58 | /** 59 | * Handle User Profile Edit Actions 60 | * 61 | * Uses the custom Response Handler to handle User Profile Edit actions. 62 | * 63 | * @param {Object} responseObject Response object from the request with alert info or redirect info. 64 | * @param {Object} data Data object required for handling the action. 65 | * @param {Function} action Redux action to handle data. 66 | */ 67 | UserProfileEditController.handleAction = (responseObject, data, action) => { 68 | // Add additional content to the response object to be handled. 69 | responseObject = { 70 | ...responseObject, 71 | setUser: true 72 | } 73 | 74 | // Use the custom Response Handler to determine what to do with the user. 75 | const response = ResponseHandler.action(responseObject, data, action); 76 | 77 | // Check if there are any errors to display. 78 | if (get(response, "type")) { 79 | // Return the error information. 80 | return response; 81 | } else { 82 | // No error, so the user was redirected. 83 | return false; 84 | } 85 | } 86 | 87 | // Export the Verify 2Factor Controller. 88 | export default UserProfileEditController; -------------------------------------------------------------------------------- /frontend/src/views/User/UserProfile/UserProfileEdit/UserProfileEditForm.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputColXL": 8, 4 | "inputColMD": 12, 5 | "inputColClassName": "mx-auto", 6 | "type": "text", 7 | "name": "firstName", 8 | "prependIcon": "user-tie", 9 | "validation": { 10 | "required": { 11 | "langKey": [ 12 | "common", 13 | "input", 14 | "firstName", 15 | "validation", 16 | "required" 17 | ] 18 | }, 19 | "minLength": { 20 | "rule": 3, 21 | "langKey": [ 22 | "common", 23 | "input", 24 | "firstName", 25 | "validation", 26 | "minLength" 27 | ] 28 | }, 29 | "max": { 30 | "rule": 50, 31 | "langKey": [ 32 | "common", 33 | "input", 34 | "firstName", 35 | "validation", 36 | "max" 37 | ] 38 | } 39 | } 40 | }, 41 | { 42 | "inputColXL": 8, 43 | "inputColMD": 12, 44 | "inputColClassName": "mx-auto", 45 | "type": "text", 46 | "name": "lastName", 47 | "prependIcon": "user-ninja", 48 | "validation": { 49 | "required": { 50 | "langKey": [ 51 | "common", 52 | "input", 53 | "lastName", 54 | "validation", 55 | "required" 56 | ] 57 | }, 58 | "minLength": { 59 | "rule": 3, 60 | "langKey": [ 61 | "common", 62 | "input", 63 | "lastName", 64 | "validation", 65 | "minLength" 66 | ] 67 | }, 68 | "max": { 69 | "rule": 100, 70 | "langKey": [ 71 | "common", 72 | "input", 73 | "lastName", 74 | "validation", 75 | "max" 76 | ] 77 | } 78 | } 79 | }, 80 | { 81 | "inputColXL": 8, 82 | "inputColMD": 12, 83 | "inputColClassName": "mx-auto", 84 | "type": "email", 85 | "name": "email", 86 | "prependIcon": "envelope", 87 | "validation": { 88 | "required": { 89 | "langKey": [ 90 | "common", 91 | "input", 92 | "email", 93 | "validation", 94 | "required" 95 | ] 96 | }, 97 | "email": { 98 | "langKey": [ 99 | "common", 100 | "input", 101 | "email", 102 | "validation", 103 | "email" 104 | ] 105 | } 106 | } 107 | }, 108 | { 109 | "inputColXL": 8, 110 | "inputColMD": 12, 111 | "inputColClassName": "mx-auto", 112 | "type": "text", 113 | "name": "username", 114 | "prependIcon": "user-secret", 115 | "validation": { 116 | "required": { 117 | "langKey": [ 118 | "common", 119 | "input", 120 | "username", 121 | "validation", 122 | "required" 123 | ] 124 | }, 125 | "minLength": { 126 | "rule": 5, 127 | "langKey": [ 128 | "common", 129 | "input", 130 | "username", 131 | "validation", 132 | "minLength" 133 | ] 134 | }, 135 | "max": { 136 | "rule": 30, 137 | "langKey": [ 138 | "common", 139 | "input", 140 | "username", 141 | "validation", 142 | "max" 143 | ] 144 | } 145 | } 146 | }, 147 | { 148 | "inputColXL": 8, 149 | "inputColMD": 12, 150 | "inputColClassName": "mx-auto", 151 | "type": "tel", 152 | "name": "mobilePhone", 153 | "maxLength": 10, 154 | "prependIcon": "phone", 155 | "validation": { 156 | "phoneOptional": { 157 | "langKey": [ 158 | "common", 159 | "input", 160 | "mobilePhone", 161 | "validation", 162 | "phoneOptional" 163 | ] 164 | } 165 | } 166 | } 167 | ] -------------------------------------------------------------------------------- /frontend/src/views/User/UserProfile/UserProfileEdit/UserProfileEditPageData.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | 4 | // Components 5 | import PageData from "../../../../components/Util/PageData"; 6 | 7 | /** 8 | * User Profile Edit Page Data Component 9 | * 10 | * Display the user's information for their profile to be able to edit it. 11 | * 12 | * @param {Boolean} isLoading Loading indication for API request. 13 | * @param {Object} results Result object from the API request. 14 | * @param {Object} hasError Error object from the API request. 15 | * @param {Function} component The component that needs to be diplayed, as a function. 16 | */ 17 | const UserProfileEditPageData = ({ isLoading, results, hasError, component }) => { 18 | // Display the user's profile information from the function provided. 19 | const profileInfo = component(); 20 | 21 | // Return the formatted result, with error and loading indication. 22 | return ; 28 | } 29 | 30 | // Export the ToDo Item Data Component. 31 | export default UserProfileEditPageData; -------------------------------------------------------------------------------- /frontend/src/views/User/UserProfile/UserProfileEdit/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import UserProfileEdit from "./UserProfileEdit"; 3 | 4 | // Export modules 5 | export default UserProfileEdit; -------------------------------------------------------------------------------- /frontend/src/views/User/UserProfile/UserProfileView/UserProfileView.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { connect } from "react-redux"; 4 | import { Link } from "react-router-dom"; 5 | import { get } from "lodash"; 6 | import classNames from "classnames"; 7 | import { 8 | Card, 9 | CardHeader, 10 | CardBody, 11 | Container, 12 | Row, 13 | Col 14 | } from "reactstrap"; 15 | 16 | // Config 17 | import { config, links } from "../../../../config"; 18 | 19 | // API 20 | import APIFetch from "../../../../util/APIFetch"; 21 | import UserProfileViewAPI from "./UserProfileViewAPI"; 22 | 23 | // Components 24 | import CustomButton from "../../../../components/CustomButton"; 25 | 26 | // Page Data 27 | import UserProfileViewPageData from "./UserProfileViewPageData"; 28 | 29 | /** 30 | * User Profile View 31 | * 32 | * Get the user's profile information from FusionAuth. 33 | * 34 | * @param {String} locale The current locale of the application. 35 | * @param {Object} languageData Current language information for the app. Language data object. 36 | */ 37 | const UserProfileView = ({ locale, languageData }) => { 38 | // Setup the API Fetch utility for the User Profile View. 39 | const [{ isLoading, results, hasError }] = APIFetch({ 40 | locale, 41 | BASEURL: config.apiServer.BASEURL, 42 | PATH_SEARCH: UserProfileViewAPI.apiService.getProfile.PATH_SEARCH, 43 | PATH_METHOD: UserProfileViewAPI.apiService.getProfile.PATH_METHOD 44 | }); 45 | 46 | // Create a dynamic class name for the panel that will center the text 47 | // if the result status is not 200. 48 | const isCenteredText = classNames({ 49 | "mx-auto": isLoading || get(results, "status") !== 200 50 | }); 51 | 52 | // Render the User Profile View. 53 | return ( 54 | 55 | 56 | 57 | 58 | 59 | { get(languageData, ["common", "myProfile"]) } 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | 81 | /** 82 | * Get App State 83 | * 84 | * Get the requried state for the component from the Redux store. 85 | * 86 | * @param {Object} state Application state from Redux. 87 | */ 88 | const mapStateToProps = state => { 89 | return { 90 | locale: state.language.locale, 91 | languageData: state.language.languageData 92 | } 93 | } 94 | 95 | // Export the User Profile View. 96 | export default connect(mapStateToProps)(UserProfileView); 97 | -------------------------------------------------------------------------------- /frontend/src/views/User/UserProfile/UserProfileView/UserProfileViewAPI.jsx: -------------------------------------------------------------------------------- 1 | // Export the User Profile View API. 2 | export default { 3 | apiService: { 4 | getProfile: { 5 | PATH_SEARCH: "/api/user/profile", 6 | PATH_METHOD: "get" 7 | } 8 | } 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/User/UserProfile/UserProfileView/UserProfileViewPageData.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from "react"; 3 | import { Col } from "reactstrap"; 4 | import { map } from "lodash"; 5 | 6 | // Components 7 | import PageData from "../../../../components/Util/PageData"; 8 | 9 | /** 10 | * User Profile View Page Data Component 11 | * 12 | * Display the user's profile data properly. 13 | * 14 | * @param {boolean} isLoading Loading indication for API request. 15 | * @param {object} results Result object from the API request. 16 | * @param {object} hasError Error object from the API request. 17 | */ 18 | const UserProfileViewPageData = ({ isLoading, results, hasError }) => { 19 | // Fields from the user object that we care about. The rest are not 20 | // wanted or needed to be seen. 21 | const fieldNames = [ 22 | { fieldName: "firstName", text: "First Name" }, 23 | { fieldName: "lastName", text: "Last Name"}, 24 | { fieldName: "email", text: "Email" }, 25 | { fieldName: "username", text: "Username" }, 26 | { fieldName: "twoFactorEnabled", text: "2FA Enabled" }, 27 | { fieldName: "mobilePhone", text: "Phone" } 28 | ]; 29 | 30 | // Display user information if the results object is set. 31 | // Loop through and find information in the result object from the fields array. 32 | const userInfo = results && map(fieldNames, (field, key) => { 33 | // Get the correct value. 34 | const value = results.data.content[field.fieldName] === true ? "true" : results.data.content[field.fieldName]; 35 | 36 | // Return the formatted component. 37 | return ( 38 | 39 | { field.text }
    40 | { value || "Not Set" } 41 | 42 | ); 43 | }); 44 | 45 | // Return the formatted result, with error and loading indication. 46 | return ; 52 | } 53 | 54 | // Export the User Profile View Page Data Component. 55 | export default UserProfileViewPageData; -------------------------------------------------------------------------------- /frontend/src/views/User/UserProfile/UserProfileView/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import UserProfileView from "./UserProfileView"; 3 | 4 | // Export modules 5 | export default UserProfileView; -------------------------------------------------------------------------------- /frontend/src/views/User/UserProfile/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import UserProfileView from "./UserProfileView"; 3 | import UserProfileEdit from "./UserProfileEdit"; 4 | 5 | // Export modules 6 | export { 7 | UserProfileView, 8 | UserProfileEdit 9 | }; -------------------------------------------------------------------------------- /frontend/src/views/User/index.jsx: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import UserChangePassword from "./UserChangePassword"; 3 | import User2Factor from "./User2Factor"; 4 | 5 | // Export modules 6 | export { 7 | UserChangePassword, 8 | User2Factor 9 | }; -------------------------------------------------------------------------------- /mongodb/roles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "route": "/user/profile/", 4 | "create": ["member"], 5 | "read": ["member"], 6 | "update": ["member"], 7 | "delete": ["member"] 8 | }, 9 | { 10 | "route": "/user/profile/edit/", 11 | "create": ["member"], 12 | "read": ["member"], 13 | "update": ["member"], 14 | "delete": ["member"] 15 | }, 16 | { 17 | "route": "/user/changePassword/", 18 | "create": ["member"], 19 | "read": ["member"], 20 | "update": ["member"], 21 | "delete": ["member"] 22 | }, 23 | { 24 | "route": "/user/2fa/", 25 | "create": ["member"], 26 | "read": ["member"], 27 | "update": ["member"], 28 | "delete": ["member"] 29 | }, 30 | { 31 | "route": "/todo/", 32 | "create": ["member"], 33 | "read": ["member"], 34 | "update": ["member"], 35 | "delete": ["member"] 36 | }, 37 | { 38 | "route": "/todo/add/", 39 | "create": ["member"], 40 | "read": ["member"], 41 | "update": ["member"], 42 | "delete": ["member"] 43 | }, 44 | { 45 | "route": "/todo/edit/:id", 46 | "create": ["member"], 47 | "read": ["member"], 48 | "update": ["member"], 49 | "delete": ["member"] 50 | }, 51 | { 52 | "route": "/todo/:id", 53 | "create": ["member"], 54 | "read": ["member"], 55 | "update": ["member"], 56 | "delete": ["member"] 57 | } 58 | ] -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .env -------------------------------------------------------------------------------- /server/.drone.yaml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | steps: 5 | - name: docker 6 | image: plugins/docker 7 | settings: 8 | username: 9 | from_secret: username 10 | password: 11 | from_secret: password 12 | repo: 13 | from_secret: repo 14 | registry: 15 | from_secret: registry 16 | tags: latest -------------------------------------------------------------------------------- /server/.example.env: -------------------------------------------------------------------------------- 1 | ## General 2 | NODE_ENV = "production" 3 | HTTP_PORT = 5000 4 | HTTPS_PORT = 5001 5 | 6 | ## Cookies 7 | COOKIES_SIGNED_SECRET = "random-uuid" 8 | 9 | ## MongoDB 10 | DATABASE_USERNAME = "demo" 11 | DATABASE_PASSWORD = "demoPass" 12 | DATABASE_HOST = "something.mongodb.net" 13 | DATABASE_NAME = "fusionAuthDemo" 14 | 15 | ## FusionAuth 16 | FUSIONAUTH_API_KEY = "Q3t_29GMZgyh_mko2rVuQ_s2qjlsZFJyJh0XirIiGVw" 17 | FUSIONAUTH_APPLICATION_ID = "10e4d908-7655-44af-abf0-1a031aff519a" 18 | FUSIONAUTH_APPLICATION_SECRET = "GUG8Hi7YzCbKUgDCRKGpPzqOrFckbNitmsYc6nGesgs" 19 | 20 | ## App Locations 21 | FUSIONAUTH_BASEURL = "http://localhost:9011" 22 | FRONTEND_BASEURL = "http://localhost:3000" -------------------------------------------------------------------------------- /server/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: docker:latest 2 | services: 3 | - docker:18.09-dind 4 | 5 | stages: 6 | - build 7 | - deploy 8 | 9 | variables: 10 | DOCKER_HOST: tcp://localhost:2375 11 | REPO_URL: docker-repo.com/fusionauth/ 12 | CONTAINER_IMAGE: service/server:${CI_COMMIT_SHORT_SHA} 13 | DEPLOYMENT_NAME: fusionauth-demo-server 14 | DEPLOYMENT_NAMESPACE: fusionauth-demo 15 | 16 | build: 17 | stage: build 18 | script: 19 | - docker login -u ${FUSIONAUTH_HARBOR_USER} -p ${FUSIONAUTH_HARBOR_PASSWORD} ${REPO_URL} 20 | - docker build -t ${CONTAINER_IMAGE} . 21 | - docker tag ${CONTAINER_IMAGE} ${REPO_URL}${CONTAINER_IMAGE} 22 | - docker push ${REPO_URL}${CONTAINER_IMAGE} 23 | 24 | deploy: 25 | stage: deploy 26 | image: 27 | name: bitnami/kubectl:latest 28 | entrypoint: [""] 29 | script: 30 | - kubectl set image deployment/${DEPLOYMENT_NAME} ${DEPLOYMENT_NAME}=${REPO_URL}${CONTAINER_IMAGE} -n ${DEPLOYMENT_NAMESPACE} 31 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | ##################### 2 | ## Stage 1 - Build ## 3 | ##################### 4 | # Use latest LTS node 5 | FROM node:10-alpine 6 | 7 | # Install app dependencies 8 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 9 | # where available (npm@5+) 10 | COPY package*.json ./ 11 | 12 | # Storing node modules on another layer will prevent unnecessary npm installs each build 13 | RUN npm ci --only=production && mkdir /app && mv ./node_modules ./app 14 | 15 | # Set working directory. 16 | WORKDIR /app 17 | 18 | # Add app to directory 19 | COPY . . 20 | 21 | # Expose APP ports 22 | EXPOSE 5000 23 | EXPOSE 5001 24 | 25 | # Run the Node application 26 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /server/Jenkinsfile: -------------------------------------------------------------------------------- 1 | podTemplate(label: "mypod", containers: [ 2 | containerTemplate(name: "docker", image: "docker", ttyEnabled: true, command: "cat"), 3 | containerTemplate(name: "node", image: "node", ttyEnabled: true, command: "cat"), 4 | containerTemplate(name: "kubectl", image: "lachlanevenson/k8s-kubectl:v1.7.3", command: "cat", ttyEnabled: true) 5 | ], 6 | volumes: [ 7 | hostPathVolume(mountPath: "/var/run/docker.sock", hostPath: "/var/run/docker.sock"), 8 | ]) { 9 | node("mypod") { 10 | def commitID 11 | def imageTag 12 | def imageName = "fusionauth/service/server" 13 | def repoURL = "docker-repo.com/fusionauth/" 14 | def deploymentName = "fusionauth-demo-server" 15 | def namespace = "fusionauth-demo" 16 | 17 | stage ("Checkout Code") { 18 | checkout scm 19 | commitID = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() 20 | imageTag = "${imageName}:${commitID}" 21 | } 22 | 23 | container("node") { 24 | stage("Test and Build") { 25 | // parallel( 26 | // "Run Tests": { 27 | // sh "chmod +x ./jenkins/test.sh" 28 | // sh "./jenkins/test.sh" 29 | // }, 30 | // "Create Build Artifacts": { 31 | // sh "npm run build" 32 | // } 33 | // ) 34 | } 35 | } 36 | 37 | container("docker") { 38 | stage("Build image") { 39 | withDockerRegistry([credentialsId: "credentials-id", url: "https://${repoURL}"]) { 40 | sh "docker build --network=host -t ${imageTag} ." 41 | sh "docker tag ${imageTag} ${repoURL}${imageTag}" 42 | sh "docker push ${repoURL}${imageTag}" 43 | } 44 | } 45 | } 46 | 47 | container("kubectl") { 48 | stage("Deploy") { 49 | withCredentials([[$class: "UsernamePasswordMultiBinding", credentialsId: "credentials-id", 50 | usernameVariable: "DOCKER_HUB_USER", 51 | passwordVariable: "DOCKER_HUB_PASSWORD"]]) { 52 | sh "kubectl set image deployment/${deploymentName} ${deploymentName}=${repoURL}${imageTag} -n ${namespace}" 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # FusionAuth NodeJS Example 2 | 3 | This is the NodeJS portion of the FusionAuth NodeJS + React Todo Example. 4 | 5 | ## About 6 | The NodeJS application uses Mongoose as an ORM for interacting with the MongoDB database. Express is used to serve an HTTP server, but an HTTPS server can easily be created as well if you provision an SSL certificate. The code is available for an HTTPS server (`src/expressServer/expressServer.js`), but the HTTPS server is never started in this demo. 7 | 8 | ## Prerequisites 9 | * Have already installed dependencies with `npm install`. 10 | 11 | ## Configuration 12 | 13 | Configuration is done in `.env`. You can copy or rename `.example.env` as a base. If you change the port, you will need to change it in the Dockerfile and Kubernetes configuration, if you deploy via that route. 14 | 15 | | Parameter | Description | Example | 16 | | ------------- | ------------- | ----- | 17 | | **General** | 18 | | NODE_ENV | The environment for the application to run on. | `production` | 19 | | HTTP_PORT | The port for the HTTP server to expose itself on. | `5000` | 20 | | HTTPS_PORT | The port for the HTTPS server to expose itself on. | `5001` | 21 | | **COOKIES** | 22 | | COOKIES_SIGNED_SECRET | A secret value to be used for signing and validating cookies used for FusionAuth authentication. | You can use something like [uuid](https://www.npmjs.com/package/uuid). | 23 | | **MongoDB** | 24 | | DATABASE_USERNAME | The name of the user created on the MongoDB database with read and write access for the application. | `demo` | 25 | | DATABASE_PASSWORD | The password for the MongoDB user. | `demoPass` | 26 | | DATABASE_HOST | The hostname of the location of the MongoDB database. Can also be ip:port or domain:port. | `something.mongodb.net` | 27 | | DATABASE_NAME | A database name for the application to use on the connection. | `fusionAuthDemo` | 28 | | **FusionAuth** | 29 | | FUSIONAUTH_API_KEY | The API key created in FusionAuth that provides access to resources in FusionAuth. | `Q3t_29GMZgyh_mko2rVuQ_s2qjlsZFJyJh0XirIiGVw` | 30 | | FUSIONAUTH_APPLICATION_ID | The generated application ID for the app created in the FusionAuth client for the demo. | `10e4d908-7655-44af-abf0-1a031aff519a` | 31 | | FUSIONAUTH_APPLICATION_SECRET | The generated application secret for the app created in the FusionAuth client for the demo. | `GUG8Hi7YzCbKUgDCRKGpPzqOrFckbNitmsYc6nGesgs` | 32 | | **App Locations** | 33 | | FUSIONAUTH_BASEURL | The URL where the FusionAuth server is hosted and set up. | `http://localhost:9011` | 34 | | FRONTEND_BASEURL | The URL of where the frontend is hosted so that it can be whitelisted and allowed access to the API. | `http://localhost:3000` | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fusionauth-nodejs-react-example", 3 | "description": "An example application using NodeJS, ExpressJS, ReactJS and FusionAuth", 4 | "version": "1.0.0", 5 | "author": "FusionAuth", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "main": "server.js", 9 | "contributors": [ 10 | { 11 | "name": "Tyler Engelhardt", 12 | "url": "https://mainelysoftware.com", 13 | "email": "tyler@mainelysoftware.com" 14 | } 15 | ], 16 | "scripts": { 17 | "start": "node server.js", 18 | "test": "mocha" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/FusionAuth/fusionauth-nodejs-react-example/issues" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/FusionAuth/fusionauth-nodejs-react-example" 26 | }, 27 | "dependencies": { 28 | "axios": "^0.19.0", 29 | "body-parser": "^1.19.0", 30 | "cookie-parser": "^1.4.4", 31 | "cors": "^2.8.5", 32 | "express": "^4.17.1", 33 | "lodash": "^4.17.15", 34 | "mongoose": "^5.7.5", 35 | "passport": "^0.4.0" 36 | }, 37 | "devDependencies": { 38 | "dotenv": "^8.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for the API 3 | * 4 | * This file brings in the express server and starts it when the app is initialized. 5 | * With this setup, you can also start background processes when the app starts. 6 | */ 7 | 8 | // Dependencies 9 | const expressServer = require("./src/expressServer"); 10 | 11 | // Declare the application module. 12 | const app = {}; 13 | 14 | /** 15 | * Application Initialization 16 | * 17 | * The application is initialized in this function. In this case, the application starts the 18 | * express server. This can be used to start background processes as well. 19 | */ 20 | app.init = () => { 21 | // Start the express server. 22 | expressServer.init(); 23 | } 24 | 25 | // Start the application. 26 | app.init(); -------------------------------------------------------------------------------- /server/src/components/fusionAuth/fusionAuthAPI.js: -------------------------------------------------------------------------------- 1 | // Export the FusionAuth API. 2 | module.exports = { 3 | fusionAuth: { 4 | postRegistration: { 5 | PATH_SEARCH: "/api/user/registration", 6 | PATH_METHOD: "post" 7 | }, 8 | verifyEmail: { 9 | PATH_SEARCH: "/api/user/verify-email", 10 | PATH_METHOD: "post" 11 | }, 12 | verify2Factor: { 13 | PATH_SEARCH: "/api/two-factor/login", 14 | PATH_METHOD: "post" 15 | }, 16 | login: { 17 | PATH_SEARCH: "/api/login", 18 | PATH_METHOD: "post" 19 | }, 20 | forgotPassword: { 21 | PATH_SEARCH: "/api/user/forgot-password", 22 | PATH_METHOD: "post" 23 | }, 24 | changePassword: { 25 | PATH_SEARCH: "/api/user/change-password", 26 | PATH_METHOD: "post" 27 | } 28 | } 29 | }; -------------------------------------------------------------------------------- /server/src/components/fusionAuth/fusionAuthInputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "register": { 3 | "username": { 4 | "required": true, 5 | "minLength": { 6 | "rule": 5 7 | }, 8 | "max": { 9 | "rule": 30 10 | } 11 | }, 12 | "firstName": { 13 | "required": true, 14 | "minLength": { 15 | "rule": 3 16 | }, 17 | "max": { 18 | "rule": 50 19 | } 20 | }, 21 | "lastName": { 22 | "required": true, 23 | "minLength": { 24 | "rule": 3 25 | }, 26 | "max": { 27 | "rule": 100 28 | } 29 | }, 30 | "email": { 31 | "required": true, 32 | "email": true 33 | }, 34 | "password": { 35 | "required": true, 36 | "minLength": { 37 | "rule": 8 38 | }, 39 | "max": { 40 | "rule": 256 41 | } 42 | } 43 | }, 44 | "verifyEmail": { 45 | "verificationId": { 46 | "length": { 47 | "rule": 43 48 | } 49 | } 50 | }, 51 | "verify2Factor": { 52 | "twoFactorId": { 53 | "required": true, 54 | "length": { 55 | "rule": 43 56 | } 57 | }, 58 | "code": { 59 | "required": true, 60 | "length": { 61 | "rule": 6 62 | } 63 | } 64 | }, 65 | "forgotPassword": { 66 | "loginId": { 67 | "required": true 68 | } 69 | }, 70 | "changePassword": { 71 | "password": { 72 | "required": true, 73 | "minLength": { 74 | "rule": 8 75 | }, 76 | "max": { 77 | "rule": 256 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /server/src/components/fusionAuth/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const fusionAuth = require("./fusionAuth"); 3 | 4 | // Export modules 5 | module.exports = fusionAuth; -------------------------------------------------------------------------------- /server/src/components/health/health.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Health Module 3 | * 4 | * This module contains functions for the /health endpoint. 5 | */ 6 | 7 | // Dependencies 8 | const express = require("express"); 9 | const router = express.Router(); 10 | const { get } = require("lodash"); 11 | 12 | // Language 13 | const language = require("../../util/language"); 14 | 15 | /** 16 | * Health Check 17 | * 18 | * Returns a 200 status ok so services know the API is available. 19 | */ 20 | router.get("/", (req, res) => { 21 | // Language data. 22 | const languageData = language.getText(req.headers.locale); 23 | 24 | // Send a 200 status ok response with text saying the API is available. Defaults 25 | // to an English message. 26 | res.send({ message: get(languageData, ["common", "app", "available"]) }); 27 | }); 28 | 29 | // Export the User API module. 30 | module.exports = router; 31 | -------------------------------------------------------------------------------- /server/src/components/health/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const health = require("./health"); 3 | 4 | // Export modules 5 | module.exports = health; -------------------------------------------------------------------------------- /server/src/components/roles/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const roles = require("./roles"); 3 | const rolesModel = require("./rolesModel"); 4 | 5 | // Export modules 6 | module.exports = { 7 | roles, 8 | rolesModel 9 | }; -------------------------------------------------------------------------------- /server/src/components/roles/roles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Roles API Module 3 | * 4 | * This module contains functions for the /api/roles endpoints. Allows for 5 | * ensuring users have access to the actions they are trying to perform. 6 | */ 7 | 8 | // Dependencies 9 | const express = require("express"); 10 | const router = express.Router(); 11 | const { get } = require("lodash"); 12 | 13 | // Config 14 | const config = require("../../config"); 15 | 16 | // Model 17 | const rolesModel = require("./rolesModel"); 18 | 19 | // Language 20 | const language = require("../../util/language"); 21 | 22 | /** 23 | * Determine page access 24 | * 25 | * This is a method the frontend uses to determine whether or not a user is 26 | * able to view a page in the application. 27 | */ 28 | router.post("/canAccessPage", (req, res) => { 29 | // Language data. 30 | const languageData = language.getText(req.headers.locale); 31 | // Grab the user registrations from the request object set in the middleware. 32 | const registrations = req.user.registrations; 33 | // Filter out the applications that aren't the one specified in the config. 34 | const application = registrations.filter(registration => registration.applicationId === config.fusionAuth.applicationId); 35 | // There should only be one object in the resulting array, since IDs are unique. Grab the 36 | // roles from that object. 37 | const userRoles = application[0].roles; 38 | 39 | // Use the Roles Model to find the route in question, sent in the body of the request. 40 | rolesModel 41 | .findOne({ route: req.body.path }) 42 | .then(route => { 43 | // Loop through the read roles to see if the user has one of them. 44 | for (const allowedReadRole of route.read) { 45 | // Check if the looped role is included in the user's selection of roles. 46 | if (userRoles.includes(allowedReadRole)) { 47 | // Send a 200 OK result if the user has one of the allowed read roles for the page. 48 | res.send(); 49 | // Return, stop looping. 50 | return; 51 | } 52 | } 53 | 54 | // Send a 401 Unauthorized error if the user does not have access. 55 | res.status(401).send({ message: get(languageData, ["common", "roles", "noAccess"]) }); 56 | }) 57 | .catch(() => { 58 | // There was an error in the request, so forbid the user from accessing the content. 59 | res.status(403).send({ message: get(languageData, ["common", "roles", "notAuthorized"]) }); 60 | }); 61 | }); 62 | 63 | // Export the User API module. 64 | module.exports = router; 65 | -------------------------------------------------------------------------------- /server/src/components/roles/rolesModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Roles Model 3 | * 4 | * This sets up the Roles Model for the application. Roles are utilized 5 | * to determine user access to view, add, edit, or delete content for different 6 | * pages. The unique identifier for the roles is the route name being accessed. 7 | */ 8 | 9 | // Dependencies 10 | const mongoose = require("mongoose") 11 | 12 | // Schema for the Roles Model. 13 | const roleSchema = new mongoose.Schema({ 14 | route: { 15 | type: String, 16 | index: true, 17 | unique: true 18 | }, 19 | create: [String], 20 | read: [String], 21 | update: [String], 22 | delete: [String] 23 | }); 24 | 25 | // Setup indexes 26 | roleSchema.index({ route: 1 }, { unique: true }); 27 | 28 | // Export the Roles Model. 29 | module.exports = mongoose.model("role", roleSchema) -------------------------------------------------------------------------------- /server/src/components/todo/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const todo = require("./todo"); 3 | const todoModel = require("./todoModel"); 4 | 5 | // Export modules 6 | module.exports = { 7 | todo, 8 | todoModel 9 | }; -------------------------------------------------------------------------------- /server/src/components/todo/todoInputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "required": true, 4 | "minLength": { 5 | "rule": 5 6 | }, 7 | "max": { 8 | "rule": 150 9 | } 10 | }, 11 | "description": { 12 | "required": true, 13 | "minLength": { 14 | "rule": 5 15 | } 16 | }, 17 | "done": { 18 | "boolean": true 19 | } 20 | } -------------------------------------------------------------------------------- /server/src/components/todo/todoModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Todo Model 3 | * 4 | * This sets up the Todo Model for the application. 5 | */ 6 | 7 | // Dependencies 8 | const mongoose = require("mongoose") 9 | 10 | // Schema for the Roles Model. 11 | const todoSchema = new mongoose.Schema({ 12 | userID: String, 13 | name: { 14 | type: String, 15 | index: true, 16 | required: true 17 | }, 18 | description: String, 19 | done: Boolean, 20 | createdAt: { type: Date, default: Date.now }, 21 | updatedAt: { type: Date, default: Date.now } 22 | }); 23 | 24 | // Setup pre-use handler for the update one function. 25 | todoSchema.pre("updateOne", function (next) { 26 | // Set the updated field to the current datetime. 27 | this.update({},{ $set: { updatedAt: new Date() } }); 28 | 29 | // Continue in the model. 30 | next(); 31 | }); 32 | 33 | // Export the Todo Model. 34 | module.exports = mongoose.model("todo", todoSchema) -------------------------------------------------------------------------------- /server/src/components/user/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const user = require("./user"); 3 | 4 | // Export modules 5 | module.exports = user; -------------------------------------------------------------------------------- /server/src/components/user/userAPI.js: -------------------------------------------------------------------------------- 1 | // Export the User API. 2 | module.exports = { 3 | fusionAuth: { 4 | getUser: { 5 | PATH_SEARCH: "/api/user", 6 | PATH_METHOD: "get" 7 | }, 8 | login: { 9 | PATH_SEARCH: "/api/login", 10 | PATH_METHOD: "post" 11 | }, 12 | changePassword: { 13 | PATH_SEARCH: "/api/user/change-password", 14 | PATH_METHOD: "post" 15 | }, 16 | get2Factor: { 17 | PATH_SEARCH: "/api/two-factor/secret", 18 | PATH_METHOD: "get" 19 | }, 20 | postEnable2Factor: { 21 | PATH_SEARCH: "/api/user/two-factor", 22 | PATH_METHOD: "post" 23 | }, 24 | deleteDisable2Factor: { 25 | PATH_SEARCH: "/api/user/two-factor", 26 | PATH_METHOD: "delete" 27 | }, 28 | saveProfile: { 29 | PATH_SEARCH: "/api/user", 30 | PATH_METHOD: "put" 31 | } 32 | } 33 | }; -------------------------------------------------------------------------------- /server/src/components/user/userInputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "changePassword": { 3 | "currentPassword": { 4 | "required": true 5 | }, 6 | "password": { 7 | "required": true, 8 | "minLength": { 9 | "rule": 8 10 | }, 11 | "max": { 12 | "rule": 256 13 | } 14 | } 15 | }, 16 | "enable2Factor": { 17 | "code": { 18 | "required": true, 19 | "length": { 20 | "rule": 6 21 | } 22 | }, 23 | "secret": { 24 | "required": true, 25 | "length": { 26 | "rule": 6 27 | } 28 | } 29 | }, 30 | "disable2Factor": { 31 | "code": { 32 | "required": true, 33 | "length": { 34 | "rule": 6 35 | } 36 | } 37 | }, 38 | "saveProfile": { 39 | "username": { 40 | "required": true, 41 | "minLength": { 42 | "rule": 5 43 | }, 44 | "max": { 45 | "rule": 30 46 | } 47 | }, 48 | "firstName": { 49 | "required": true, 50 | "minLength": { 51 | "rule": 3 52 | }, 53 | "max": { 54 | "rule": 50 55 | } 56 | }, 57 | "lastName": { 58 | "required": true, 59 | "minLength": { 60 | "rule": 3 61 | }, 62 | "max": { 63 | "rule": 100 64 | } 65 | }, 66 | "email": { 67 | "required": true, 68 | "email": true 69 | }, 70 | "mobilePhone": { 71 | "phoneOptional": true 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /server/src/config/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Config Module 3 | * 4 | * The application's config is exported here using the environment variables 5 | * passed to the application. Use docker compose files, .env files, or 6 | * kubernetes config files to store managed environments. 7 | */ 8 | 9 | // Comment out or remove the following line if you wish to deploy via a Docker image. 10 | // This is set here to allow for using a .env file. 11 | require('dotenv').config(); 12 | 13 | // Create the config module. 14 | const config = { 15 | // Environment name 16 | envName: process.env.NODE_ENV, 17 | // HTTP Port: Used for creating a HTTP server. 18 | httpPort: process.env.HTTP_PORT, 19 | // HTTPS Port: Used for creating a HTTPS server (not used in this demo). 20 | httpsPort: process.env.HTTPS_PORT, 21 | cookies: { 22 | // UUID or Random string for cookie secret 23 | signedSecret: process.env.COOKIES_SIGNED_SECRET 24 | }, 25 | database: { 26 | // Database host: ip:port or server-name.tld:port 27 | host: process.env.DATABASE_HOST, 28 | // Database usename 29 | user: process.env.DATABASE_USERNAME, 30 | // Database user password 31 | password: process.env.DATABASE_PASSWORD, 32 | // MongoDB Database name 33 | database: process.env.DATABASE_NAME 34 | }, 35 | fusionAuth: { 36 | // Application API Key from FusionAuth 37 | apiKey: process.env.FUSIONAUTH_API_KEY, 38 | // Application ID from FusionAuth 39 | applicationId: process.env.FUSIONAUTH_APPLICATION_ID, 40 | // Application Secret from FusionAuth 41 | applicationSecret: process.env.FUSIONAUTH_APPLICATION_SECRET, 42 | // FusionAuth URL: http://localhost:9011 43 | baseURL: process.env.FUSIONAUTH_BASEURL 44 | }, 45 | frontend: { 46 | // React APP URL: http://localhost:3000 47 | baseURL: process.env.FRONTEND_BASEURL 48 | } 49 | }; 50 | 51 | // Export the config. 52 | module.exports = config; -------------------------------------------------------------------------------- /server/src/config/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const config = require("./config"); 3 | 4 | // Export modules 5 | module.exports = config; -------------------------------------------------------------------------------- /server/src/data/fusionAuth/fusionAuthData.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FusionAuth Data 3 | * 4 | * FusionAuth Data information is here in regards to API messages 5 | * and the content that correlates with the messages. 6 | */ 7 | 8 | // Create the FusionAuth Data module. 9 | const fusionAuthData = { 10 | "[previouslyUsed]user.password": 0, 11 | "[tooYoung]user.password": 0, 12 | "[tooShort]user.password": 8, 13 | "[tooLong]user.password": 256 14 | }; 15 | 16 | // Export the FusionAuth Data module. 17 | module.exports = fusionAuthData; -------------------------------------------------------------------------------- /server/src/data/fusionAuth/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const fusionAuthData = require("./fusionAuthData"); 3 | 4 | // Export modules 5 | module.exports = fusionAuthData; -------------------------------------------------------------------------------- /server/src/data/language/en/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const common = require("./common"); 3 | 4 | // Export modules 5 | module.exports = { 6 | common 7 | }; -------------------------------------------------------------------------------- /server/src/data/language/es/common.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FusionAuth/fusionauth-nodejs-react-example/b0a2ba366c1d7c4fbcd507a296a0845d072221a6/server/src/data/language/es/common.json -------------------------------------------------------------------------------- /server/src/data/language/es/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const common = require("./common"); 3 | 4 | // Export modules 5 | module.exports = { 6 | common 7 | }; -------------------------------------------------------------------------------- /server/src/database/database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Database Module 3 | * 4 | * Handle database actions for the application. 5 | */ 6 | 7 | // Dependencies 8 | const mongoose = require("mongoose"); 9 | 10 | // Config 11 | const config = require("../config"); 12 | 13 | // Shorthand for database proprties. 14 | const db = config.database; 15 | 16 | // Create the Database Module. 17 | const database = {}; 18 | 19 | /** 20 | * Connect to the database 21 | * 22 | * Attempt to connect to the database with the credentials and database provided. Make 23 | * sure we use "useNewUrlParser" in the options due to Mongo v4. 24 | */ 25 | database.connect = () => { 26 | // Return a promise (resolved) after a successful connection, or reject 27 | // if there was an error connecting to the database. 28 | return new Promise((resolve, reject) => { 29 | mongoose.connect(`mongodb+srv://${ db.user }:${ db.password }@${ db.host }/${ db.database }`, { useCreateIndex: true, useNewUrlParser: true }) 30 | .then(() => resolve()) 31 | .catch(() => reject()); 32 | }); 33 | }; 34 | 35 | // Export the Database Module. 36 | module.exports = database; -------------------------------------------------------------------------------- /server/src/database/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const mongodb = require("./database"); 3 | 4 | // Export modules 5 | module.exports = mongodb; -------------------------------------------------------------------------------- /server/src/expressServer/expressAPI.js: -------------------------------------------------------------------------------- 1 | // Export the Express API. 2 | module.exports = { 3 | fusionAuth: { 4 | validateToken: { 5 | PATH_SEARCH: "/api/jwt/validate", 6 | PATH_METHOD: "get" 7 | }, 8 | refreshToken: { 9 | PATH_SEARCH: "/api/jwt/refresh", 10 | PATH_METHOD: "post" 11 | }, 12 | getUser: { 13 | PATH_SEARCH: "/api/user", 14 | PATH_METHOD: "get" 15 | } 16 | } 17 | }; -------------------------------------------------------------------------------- /server/src/expressServer/expressRoutes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Express Routes Module 3 | * 4 | * The route module handles all incoming requests to the server and routes appropriately 5 | * to the correct route functions. 6 | */ 7 | 8 | // Middleware 9 | const expressMiddleware = require("./expressMiddleware"); 10 | 11 | // Route info 12 | const health = require('../components/health'); 13 | const fusionAuth = require('../components/fusionAuth'); 14 | const { roles } = require("../components/roles"); 15 | const user = require('../components/user'); 16 | const { todo } = require('../components/todo'); 17 | 18 | /** 19 | * Route Handler 20 | * 21 | * The function handles all routes and passes requests to their 22 | * appropriate route handler. 23 | * 24 | * @param {Object} app The express application 25 | */ 26 | const routes = app => { 27 | // Health check 28 | app.use("/health", health); 29 | 30 | // Authentication routes first. 31 | app.use("/api/fusionAuth", fusionAuth); 32 | // Validate JWT tokens. 33 | app.use(expressMiddleware.jwtValidate); 34 | // Grab the current user's info to ensure accurary. 35 | app.use(expressMiddleware.getCurrentUser); 36 | // Determine if a user can access a given page. 37 | app.use("/api/roles", roles); 38 | // Rest of the API endpoints. 39 | app.use("/api/user", user); 40 | app.use("/api/todo", todo); 41 | } 42 | 43 | // Export the express routes module. 44 | module.exports = routes; -------------------------------------------------------------------------------- /server/src/expressServer/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const expressServer = require("./expressServer"); 3 | 4 | // Export modules 5 | module.exports = expressServer; -------------------------------------------------------------------------------- /server/src/util/apiFetch/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const apiFetch = require("./apiFetch"); 3 | 4 | // Export modules 5 | module.exports = apiFetch; -------------------------------------------------------------------------------- /server/src/util/auth/auth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Auth Module 3 | * 4 | * This module handles authentication requirements for the API Service. 5 | */ 6 | 7 | // Declare the Auth module 8 | const auth = {}; 9 | 10 | /** 11 | * Set refresh and access tokens 12 | * 13 | * Allows refresh and access tokens to be set so that the user has persisted access 14 | * to the application. 15 | * 16 | * @param {String} refreshToken FA refresh token. 17 | * @param {String} accessToken FA access token. 18 | * @param {Object} res The response object of the request. 19 | */ 20 | auth.setCookies = (refreshToken, accessToken, res) => { 21 | // Cookie settings. In a production application, you want to set 22 | // secure: true, and if possible, sameSite: true/strict. 23 | const cookieSettings = { httpOnly: true, signed: true }; 24 | 25 | // Set the JWT and refresh cookies. 26 | res.cookie("refresh_token", refreshToken, cookieSettings); 27 | res.cookie("access_token", accessToken, cookieSettings); 28 | } 29 | 30 | /** 31 | * Remove refresh and access tokens 32 | * 33 | * Removes the access and refresh token so that the user will no longer have access 34 | * to the application. 35 | * 36 | * @param {Object} res The response object of the request. 37 | */ 38 | auth.removeCookies = (res) => { 39 | // Cookie settings 40 | const cookieSettings = { httpOnly: true, signed: true }; 41 | 42 | // Remove the JWT and refresh cookies. 43 | res.clearCookie("refresh_token", cookieSettings); 44 | res.clearCookie("access_token", cookieSettings); 45 | } 46 | 47 | // Export the Auth module. 48 | module.exports = auth; -------------------------------------------------------------------------------- /server/src/util/auth/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const auth = require("./auth"); 3 | 4 | // Export modules 5 | module.exports = auth; -------------------------------------------------------------------------------- /server/src/util/language/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const language = require("./language"); 3 | 4 | // Export modules 5 | module.exports = language; -------------------------------------------------------------------------------- /server/src/util/language/language.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Language Module 3 | * 4 | * This module will get language data for the API endpoints. 5 | */ 6 | 7 | // Declare the language module. 8 | const language = {}; 9 | 10 | /** 11 | * Get text from the language files 12 | * 13 | * Get the proper text for a given language. English is 14 | * the default language. 15 | */ 16 | language.getText = lang => { 17 | // Setup the return object. 18 | let langData = {}; 19 | 20 | // Return data for the appropriate language. 21 | switch (lang) { 22 | case "es": 23 | langData = require("../../data/language/es"); 24 | break; 25 | default: 26 | langData = require("../../data/language/en"); 27 | } 28 | 29 | // Return the language data for the selected language. 30 | return langData; 31 | } 32 | 33 | // Export the language module. 34 | module.exports = language; -------------------------------------------------------------------------------- /server/src/util/validForm/index.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | const validForm = require("./validForm"); 3 | 4 | // Export modules 5 | module.exports = validForm; --------------------------------------------------------------------------------