├── .env.example ├── .eslintignore ├── .eslintrc ├── .firebaserc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── production-deployment.yml │ ├── pull-requests.yml │ └── staging-deployment.yml ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── database.rules.json ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── functions ├── .gitignore ├── env.example.json ├── package-lock.json ├── package.json ├── setupProject.js ├── src │ ├── db │ │ └── users │ │ │ ├── onDelete.function.ts │ │ │ └── onUpdate.function.ts │ ├── firestore │ │ └── users │ │ │ ├── onDelete.function.ts │ │ │ └── onUpdate.function.ts │ ├── https │ │ └── createUser.function.ts │ ├── index.ts │ └── types │ │ └── firebase-function-tools.d.ts ├── test │ ├── db │ │ └── users │ │ │ ├── onDelete.test.ts │ │ │ └── onUpdate.test.ts │ ├── firestore │ │ └── users │ │ │ ├── onDelete.test.ts │ │ │ └── onUpdate.test.ts │ ├── https │ │ └── createUser.test.ts │ └── util │ │ └── config.ts ├── tsconfig.json └── tslint.json ├── jsconfig.json ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── assets │ ├── 404.gif │ ├── css │ │ └── main.css │ ├── default-image-establishment.jpg │ ├── en.png │ ├── es.png │ └── user-default-log.svg ├── components │ ├── .gitkeep │ ├── ConfirmationModal │ │ ├── ConfirmationModal.scss │ │ ├── ConfirmationModal.test.js │ │ ├── __snapshots__ │ │ │ └── ConfirmationModal.test.js.snap │ │ └── index.jsx │ ├── DatePicker │ │ ├── DatePicker.scss │ │ ├── DatePicker.test.js │ │ ├── __snapshots__ │ │ │ └── DatePicker.test.js.snap │ │ └── index.jsx │ ├── ErrorMessage │ │ ├── ErrorMessage.test.js │ │ ├── __snapshots__ │ │ │ └── ErrorMessage.test.js.snap │ │ └── index.jsx │ ├── LanguageWrapper │ │ └── index.js │ ├── Layout │ │ ├── Layout.module.scss │ │ ├── Layout.test.js │ │ ├── __snapshots__ │ │ │ └── Layout.test.js.snap │ │ └── index.jsx │ ├── Navigation │ │ ├── Aside │ │ │ ├── Aside.module.scss │ │ │ ├── Aside.test.js │ │ │ ├── __snapshots__ │ │ │ │ └── Aside.test.js.snap │ │ │ └── index.jsx │ │ ├── Footer │ │ │ ├── Footer.module.scss │ │ │ ├── Footer.test.js │ │ │ ├── __snapshots__ │ │ │ │ └── Footer.test.js.snap │ │ │ └── index.jsx │ │ ├── Link │ │ │ ├── Link.test.js │ │ │ ├── __snapshots__ │ │ │ │ └── Link.test.js.snap │ │ │ └── index.jsx │ │ └── NavBar │ │ │ ├── NavBar.test.js │ │ │ ├── __snapshots__ │ │ │ └── NavBar.test.js.snap │ │ │ └── index.jsx │ ├── Table │ │ ├── Table.module.scss │ │ ├── TableMobile.css │ │ └── index.jsx │ └── UserForm │ │ ├── UserForm.scss │ │ ├── UserForm.test.js │ │ ├── __snapshots__ │ │ └── UserForm.test.js.snap │ │ └── index.jsx ├── firebase.js ├── hooks │ └── index.js ├── index.js ├── index.scss ├── languages │ ├── en.json │ └── es.json ├── pages │ ├── Home │ │ ├── Home.test.js │ │ ├── __snapshots__ │ │ │ └── Home.test.js.snap │ │ └── index.jsx │ ├── Login │ │ ├── Login.module.scss │ │ ├── Login.test.js │ │ ├── __snapshots__ │ │ │ └── Login.test.js.snap │ │ └── index.jsx │ ├── NotFound │ │ ├── NotFound.module.scss │ │ ├── NotFound.test.js │ │ ├── __snapshots__ │ │ │ └── NotFound.test.js.snap │ │ └── index.jsx │ ├── Profile │ │ ├── ChangePassword │ │ │ ├── ChangePassword.test.js │ │ │ ├── __snapshots__ │ │ │ │ └── ChangePassword.test.js.snap │ │ │ └── index.jsx │ │ ├── Profile.test.js │ │ ├── __snapshots__ │ │ │ └── Profile.test.js.snap │ │ └── index.jsx │ ├── ResetPassword │ │ ├── ResetPassword.module.scss │ │ ├── ResetPassword.test.js │ │ ├── __snapshots__ │ │ │ └── ResetPassword.test.js.snap │ │ └── index.jsx │ ├── Router │ │ ├── PrivateRoute │ │ │ ├── PrivateRoute.test.js │ │ │ ├── __snapshots__ │ │ │ │ └── PrivateRoute.test.js.snap │ │ │ └── index.js │ │ ├── Router.test.js │ │ ├── __snapshots__ │ │ │ └── Router.test.js.snap │ │ ├── index.js │ │ └── paths.js │ ├── Section │ │ ├── Section.test.js │ │ ├── __snapshots__ │ │ │ └── Section.test.js.snap │ │ └── index.jsx │ ├── Submenu │ │ ├── Submenu.test.js │ │ ├── __snapshots__ │ │ │ └── Submenu.test.js.snap │ │ └── index.jsx │ ├── User │ │ ├── User.test.js │ │ ├── __snapshots__ │ │ │ └── User.test.js.snap │ │ └── index.jsx │ └── Users │ │ ├── Users.module.scss │ │ ├── Users.test.js │ │ ├── __snapshots__ │ │ └── Users.test.js.snap │ │ └── index.jsx ├── serviceWorker.js ├── setupTests.js ├── state │ ├── actions │ │ ├── auth.js │ │ ├── preferences.js │ │ └── users.js │ ├── api │ │ ├── firestore.js │ │ ├── index.js │ │ └── rtdb.js │ ├── reducers │ │ ├── auth │ │ │ ├── auth.test.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── preferences │ │ │ ├── index.js │ │ │ └── preferences.test.js │ │ └── users │ │ │ ├── index.js │ │ │ └── users.test.js │ └── store.js ├── styles │ └── .gitkeep └── utils │ └── index.js └── storage.rules /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_FIRE_BASE_KEY = '' 2 | REACT_APP_FIRE_BASE_PROJECT_ID = '' 3 | REACT_APP_FIRE_BASE_AUTH_DOMAIN = '' 4 | REACT_APP_FIRE_BASE_DB_URL = '' 5 | REACT_APP_FIRE_BASE_STORAGE_BUCKET = '' 6 | REACT_APP_FIRE_BASE_MESSAGING_SENDER_ID = '' 7 | REACT_APP_FIRE_BASE_APP_ID = '' 8 | REACT_APP_FIRE_BASE_MEASURMENT_ID = '' 9 | REACT_APP_LOGIN_PAGE_URL = 'http://localhost:3000/login' 10 | REACT_APP_FIRE_BASE_STORAGE_API = 'https://firebasestorage.googleapis.com/v0/b/${REACT_APP_FIRE_BASE_STORAGE_BUCKET}' 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "prettier", 5 | "prettier/react", 6 | "plugin:jest/recommended" 7 | ], 8 | "plugins": ["prettier", "jest"], 9 | "rules": { 10 | "react/jsx-filename-extension": [ 11 | 1, 12 | { 13 | "extensions": [".js", ".jsx"] 14 | } 15 | ], 16 | "react/prop-types": 0, 17 | "no-underscore-dangle": 0, 18 | "import/imports-first": ["error", "absolute-first"], 19 | "import/newline-after-import": "error", 20 | "import/prefer-default-export": 0, 21 | "semi": "error", 22 | "jsx-a11y/click-events-have-key-events": 0, 23 | "jsx-a11y/no-static-element-interactions": 0, 24 | "jsx-a11y/anchor-is-valid": 0, 25 | "react/require-default-props": 0, 26 | "jest/expect-expect": 0, 27 | "import/no-cycle": 0, 28 | "react/button-has-type": 0 29 | }, 30 | "globals": { 31 | "window": true, 32 | "document": true, 33 | "localStorage": true, 34 | "FormData": true, 35 | "FileReader": true, 36 | "Blob": true, 37 | "navigator": true, 38 | "Headers": true, 39 | "Request": true, 40 | "fetch": true, 41 | "reducerTester": true, 42 | "renderWithProviders": true 43 | }, 44 | "parser": "babel-eslint", 45 | "settings": { 46 | "import/resolver": { 47 | "node": { 48 | "paths": ["src"] 49 | } 50 | } 51 | }, 52 | "env": { 53 | "mocha": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "production": "react-firebase-admin-eeac2", 4 | "staging": "react-firebase-admin-eeac2", 5 | "default": "react-firebase-admin-eeac2" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | 8 | - package-ecosystem: 'npm' 9 | directory: '/functions' 10 | schedule: 11 | interval: 'weekly' 12 | 13 | - package-ecosystem: 'github-actions' 14 | directory: '/' 15 | schedule: 16 | interval: 'weekly' 17 | -------------------------------------------------------------------------------- /.github/workflows/production-deployment.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-test-deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2.3.2 14 | - name: Setup Node.js environment 15 | uses: actions/setup-node@v2-beta 16 | with: 17 | node-version: '12.x' 18 | - name: npm install, build, and test 19 | run: | 20 | npm ci 21 | cd functions 22 | npm ci 23 | cd .. 24 | npm run build 25 | env: 26 | REACT_APP_FIRE_BASE_KEY: ${{ secrets.FIRE_BASE_KEY_STAGING }} 27 | REACT_APP_FIRE_BASE_AUTH_DOMAIN: ${{ secrets.FIRE_BASE_AUTH_DOMAIN_STAGING }} 28 | REACT_APP_FIRE_BASE_DB_URL: ${{ secrets.FIRE_BASE_DB_URL_STAGING }} 29 | REACT_APP_FIRE_BASE_PROJECT_ID: ${{ secrets.FIRE_BASE_PROJECT_ID_STAGING }} 30 | REACT_APP_FIRE_BASE_STORAGE_BUCKET: ${{ secrets.FIRE_BASE_STORAGE_BUCKET_STAGING }} 31 | REACT_APP_FIRE_BASE_MESSAGING_SENDER_ID: ${{ secrets.FIRE_BASE_MESSAGING_SENDER_ID_STAGING }} 32 | REACT_APP_FIRE_BASE_APP_ID: ${{ secrets.FIRE_BASE_APP_ID_STAGING }} 33 | REACT_APP_FIRE_BASE_MEASURMENT_ID: ${{ secrets.FIRE_BASE_MEASURMENT_ID_STAGING }} 34 | REACT_APP_CLOUD_FUNCTIONS_REST_API: ${{ secrets.CLOUD_FUNCTIONS_REST_API_STAGING }} 35 | REACT_APP_LOGIN_PAGE_URL: ${{ secrets.LOGIN_PAGE_URL_STAGING }} 36 | CI: '' 37 | - name: Firebase deployment 38 | run: | 39 | npm install -g firebase-tools 40 | firebase deploy -P staging --token $FIREBASE_TOKEN 41 | env: 42 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/pull-requests.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - development 8 | - feature/* 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2.3.2 18 | with: 19 | ref: ${{ github.ref }} 20 | - name: Setup Node.js environment 21 | uses: actions/setup-node@v2-beta 22 | with: 23 | node-version: '12.x' 24 | - name: Installing dependencies 25 | run: npm ci 26 | - name: Building project 27 | run: npm run build 28 | env: 29 | CI: '' 30 | tests: 31 | name: Testing 32 | 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v2.3.2 37 | - name: Setup Node.js environment 38 | uses: actions/setup-node@v2-beta 39 | with: 40 | node-version: '12.x' 41 | - name: Installing dependencies 42 | run: npm ci 43 | - name: Running project tests 44 | run: CI=true npm test 45 | env: 46 | REACT_APP_FIRE_BASE_KEY: ${{ secrets.FIREBASE_TOKEN }} 47 | REACT_APP_FIRE_BASE_AUTH_DOMAIN: ${{ secrets.FIRE_BASE_AUTH_DOMAIN_STAGING }} 48 | REACT_APP_FIRE_BASE_DB_URL: ${{ secrets.FIRE_BASE_DB_URL_STAGING }} 49 | REACT_APP_FIRE_BASE_PROJECT_ID: ${{ secrets.FIRE_BASE_PROJECT_ID_STAGING }} 50 | REACT_APP_FIRE_BASE_STORAGE_BUCKET: ${{ secrets.FIRE_BASE_STORAGE_BUCKET_STAGING }} 51 | REACT_APP_FIRE_BASE_MESSAGING_SENDER_ID: ${{ secrets.FIRE_BASE_MESSAGING_SENDER_ID_STAGING }} 52 | REACT_APP_FIRE_BASE_APP_ID: ${{ secrets.FIRE_BASE_APP_ID_STAGING }} 53 | REACT_APP_FIRE_BASE_MEASURMENT_ID: ${{ secrets.FIRE_BASE_MEASURMENT_ID_STAGING }} 54 | REACT_APP_CLOUD_FUNCTIONS_REST_API: ${{ secrets.CLOUD_FUNCTIONS_REST_API_STAGING }} 55 | REACT_APP_LOGIN_PAGE_URL: ${{ secrets.LOGIN_PAGE_URL_STAGING }} 56 | -------------------------------------------------------------------------------- /.github/workflows/staging-deployment.yml: -------------------------------------------------------------------------------- 1 | name: Staging CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - development 7 | 8 | jobs: 9 | build-test-deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2.3.2 14 | - name: Setup Node.js environment 15 | uses: actions/setup-node@v2-beta 16 | with: 17 | node-version: '12.x' 18 | - name: npm install, build, and test 19 | run: | 20 | npm ci 21 | cd functions 22 | npm ci 23 | cd .. 24 | npm run build 25 | env: 26 | REACT_APP_FIRE_BASE_KEY: ${{ secrets.FIRE_BASE_KEY_STAGING }} 27 | REACT_APP_FIRE_BASE_AUTH_DOMAIN: ${{ secrets.FIRE_BASE_AUTH_DOMAIN_STAGING }} 28 | REACT_APP_FIRE_BASE_DB_URL: ${{ secrets.FIRE_BASE_DB_URL_STAGING }} 29 | REACT_APP_FIRE_BASE_PROJECT_ID: ${{ secrets.FIRE_BASE_PROJECT_ID_STAGING }} 30 | REACT_APP_FIRE_BASE_STORAGE_BUCKET: ${{ secrets.FIRE_BASE_STORAGE_BUCKET_STAGING }} 31 | REACT_APP_FIRE_BASE_MESSAGING_SENDER_ID: ${{ secrets.FIRE_BASE_MESSAGING_SENDER_ID_STAGING }} 32 | REACT_APP_FIRE_BASE_APP_ID: ${{ secrets.FIRE_BASE_APP_ID_STAGING }} 33 | REACT_APP_FIRE_BASE_MEASURMENT_ID: ${{ secrets.FIRE_BASE_MEASURMENT_ID_STAGING }} 34 | REACT_APP_CLOUD_FUNCTIONS_REST_API: ${{ secrets.CLOUD_FUNCTIONS_REST_API_STAGING }} 35 | REACT_APP_LOGIN_PAGE_URL: ${{ secrets.LOGIN_PAGE_URL_STAGING }} 36 | CI: '' 37 | - name: Firebase deployment 38 | run: | 39 | npm install -g firebase-tools 40 | firebase deploy -P staging --token $FIREBASE_TOKEN 41 | env: 42 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 63 | 64 | # dependencies 65 | /node_modules 66 | /.pnp 67 | .pnp.js 68 | 69 | # testing 70 | /coverage 71 | 72 | # production 73 | /build 74 | 75 | # misc 76 | .DS_Store 77 | .env.local 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | 82 | npm-debug.log* 83 | yarn-debug.log* 84 | yarn-error.log* 85 | 86 | .firebase 87 | 88 | # service account key 89 | serviceAccountKey.json 90 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CreateThrive 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "users": { 4 | "$user": { 5 | ".write": "auth !== null && ((auth.token.isAdmin === true && data.child('isAdmin').val() === false) || auth.uid === $user)", 6 | ".read": "auth !== null && ((auth.token.isAdmin === true && data.child('isAdmin').val() === false) || auth.uid === $user)" 7 | }, 8 | ".write": "auth !== null && auth.token.isAdmin === true", 9 | ".read": "auth !== null && auth.token.isAdmin === true" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "build", 7 | "ignore": [ 8 | "firebase.json", 9 | "**/.*", 10 | "**/node_modules/**" 11 | ], 12 | "rewrites": [ 13 | { 14 | "source": "**", 15 | "destination": "/index.html" 16 | } 17 | ] 18 | }, 19 | "storage": { 20 | "rules": "storage.rules" 21 | }, 22 | "functions": { 23 | "predeploy": [ 24 | "npm --prefix \"$RESOURCE_DIR\" run lint", 25 | "npm --prefix \"$RESOURCE_DIR\" run build" 26 | ], 27 | "source": "functions" 28 | }, 29 | "firestore": { 30 | "rules": "firestore.rules", 31 | "indexes": "firestore.indexes.json" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | 5 | function isAuthenticated() { 6 | return request.auth != null && request.auth.token.email_verified == true; 7 | } 8 | 9 | function isAdmin() { 10 | return request.auth.token.isAdmin == true; 11 | } 12 | 13 | 14 | match /users/{userId} { 15 | 16 | function isIdentified() { 17 | return request.auth.uid == userId; 18 | } 19 | 20 | allow read: if isAuthenticated() && (isAdmin() || isIdentified()); 21 | 22 | allow write: if isAuthenticated() && isAdmin(); 23 | 24 | allow update: if isAuthenticated() && (isAdmin() || isIdentified()); 25 | 26 | allow delete: if isAuthenticated() && isAdmin(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Typescript v1 declaration files 6 | typings/ 7 | 8 | node_modules/ 9 | 10 | lib/ 11 | 12 | #Necessary config for testing 13 | env.json 14 | 15 | service-account-key.json -------------------------------------------------------------------------------- /functions/env.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "databaseURL": "", 3 | "storageBucket": "", 4 | "projectId": "", 5 | "serviceAccountKey": "" 6 | } 7 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase emulators:start --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log", 11 | "setup-firebase": "node setupProject.js", 12 | "test": "mocha -r ts-node/register --recursive 'test/**/*.test.ts' --timeout 10000 --exit" 13 | }, 14 | "engines": { 15 | "node": "10" 16 | }, 17 | "main": "lib/index.js", 18 | "dependencies": { 19 | "camelcase": "^6.0.0", 20 | "firebase-admin": "^9.4.1", 21 | "firebase-function-tools": "^1.1.4", 22 | "firebase-functions": "^3.12.0", 23 | "glob": "^7.1.6", 24 | "inquirer": "^7.3.3" 25 | }, 26 | "devDependencies": { 27 | "@types/chai": "^4.2.12", 28 | "@types/chai-as-promised": "^7.1.3", 29 | "@types/mocha": "^8.0.4", 30 | "chai": "^4.2.0", 31 | "chai-as-promised": "^7.1.1", 32 | "firebase-functions-test": "^0.2.3", 33 | "mocha": "^8.2.1", 34 | "ts-node": "^9.1.1", 35 | "tslint": "^6.1.3", 36 | "typescript": "^4.1.2" 37 | }, 38 | "private": true 39 | } 40 | -------------------------------------------------------------------------------- /functions/setupProject.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | /* eslint-disable consistent-return */ 3 | /* eslint-disable import/no-dynamic-require */ 4 | /* eslint-disable global-require */ 5 | const admin = require('firebase-admin'); 6 | const inquirer = require('inquirer'); 7 | const fs = require('fs'); 8 | 9 | const configPath = '../src/state/api/index.js'; 10 | 11 | const importPath = '../src/firebase.js'; 12 | 13 | const questions = [ 14 | { 15 | type: 'input', 16 | name: 'path', 17 | message: 'Enter the path to the service account key file: ', 18 | }, 19 | { 20 | type: 'input', 21 | name: 'databaseURL', 22 | message: 'Enter database URL: ', 23 | }, 24 | { 25 | type: 'input', 26 | name: 'email', 27 | message: 'Enter user email: ', 28 | }, 29 | { 30 | type: 'password', 31 | name: 'password', 32 | message: 'Enter user password: ', 33 | mask: '*', 34 | }, 35 | { 36 | type: 'list', 37 | name: 'database', 38 | message: 'Select the database of your choice:', 39 | choices: ['Realtime Database', 'Firestore'], 40 | }, 41 | 42 | { 43 | type: 'confirm', 44 | name: 'deletedb', 45 | message: 'Do you want to delete unused cloud functions?', 46 | default: true, 47 | }, 48 | ]; 49 | 50 | const replaceDatabase = (oldDatabase, newDatabase) => { 51 | fs.readFile(configPath, 'utf8', (error, data) => { 52 | if (error) { 53 | return console.log(error); 54 | } 55 | const result = data.replace(oldDatabase, newDatabase); 56 | 57 | fs.writeFile(configPath, result, 'utf8', (err) => { 58 | if (err) return console.log(err); 59 | }); 60 | }); 61 | 62 | fs.readFile(importPath, 'utf8', (error, data) => { 63 | if (error) { 64 | return console.log(error); 65 | } 66 | 67 | let oldInit; 68 | let newInit; 69 | let oldImport; 70 | let newImport; 71 | if (oldDatabase === 'rtdb') { 72 | oldInit = 'firebase.database()'; 73 | newInit = 'firebase.firestore()'; 74 | oldImport = 'firebase/database'; 75 | newImport = 'firebase/firestore'; 76 | } else { 77 | oldInit = 'firebase.firestore()'; 78 | newInit = 'firebase.database()'; 79 | oldImport = 'firebase/firestore'; 80 | newImport = 'firebase/database'; 81 | } 82 | 83 | data = data.replace(oldInit, newInit); 84 | 85 | data = data.replace(oldImport, newImport); 86 | 87 | fs.writeFile(importPath, data, 'utf8', (err) => { 88 | if (err) return console.log(err); 89 | }); 90 | }); 91 | }; 92 | 93 | const deleteDatabase = async (database) => { 94 | const dir = database !== 'Firestore' ? 'firestore' : 'db'; 95 | 96 | try { 97 | fs.rmdirSync(`./src/${dir}`, { recursive: true }); 98 | } catch (error) { 99 | console.error(`Error while deleting ${database}. ${error}`); 100 | } 101 | 102 | try { 103 | fs.rmdirSync(`./test/${dir}`, { recursive: true }); 104 | } catch (error) { 105 | console.error(`Error while deleting ${database} tests. ${error}`); 106 | } 107 | }; 108 | 109 | inquirer 110 | .prompt(questions) 111 | .then(async ({ database, path, email, password, databaseURL, deletedb }) => { 112 | const serviceAccount = require(path); 113 | 114 | admin.initializeApp({ 115 | credential: admin.credential.cert(serviceAccount), 116 | databaseURL, 117 | }); 118 | 119 | console.log('Setting admin account in authentication 🔨'); 120 | 121 | const { uid } = await admin.auth().createUser({ 122 | email, 123 | password, 124 | emailVerified: true, 125 | }); 126 | 127 | await admin.auth().setCustomUserClaims(uid, { 128 | isAdmin: true, 129 | }); 130 | 131 | console.log('Created admin account in authentication'); 132 | 133 | console.log('Creating admin account in database'); 134 | 135 | const user = { 136 | isAdmin: true, 137 | name: 'Test Name', 138 | location: 'Test Location', 139 | createdAt: new Date().toDateString(), 140 | email, 141 | }; 142 | 143 | if (database === 'Firestore') { 144 | replaceDatabase('rtdb', 'firestore'); 145 | await admin.firestore().collection('users').doc(uid).set(user); 146 | } else { 147 | replaceDatabase('firestore', 'rtdb'); 148 | await admin.database().ref(`users/${uid}`).set(user); 149 | } 150 | 151 | if (deletedb) { 152 | deleteDatabase(database); 153 | } 154 | 155 | console.log(`Created admin account in ${database}`); 156 | process.exit(0); 157 | }) 158 | .catch((error) => { 159 | console.log(error.message); 160 | process.exit(0); 161 | }); 162 | -------------------------------------------------------------------------------- /functions/src/db/users/onDelete.function.ts: -------------------------------------------------------------------------------- 1 | import { auth } from 'firebase-admin'; 2 | import { database } from 'firebase-functions'; 3 | 4 | export default database.ref('users/{uid}').onDelete((snapshot, context) => { 5 | const { uid } = context.params; 6 | return auth().deleteUser(uid); 7 | }); 8 | -------------------------------------------------------------------------------- /functions/src/db/users/onUpdate.function.ts: -------------------------------------------------------------------------------- 1 | import { database } from 'firebase-functions'; 2 | import { auth } from 'firebase-admin'; 3 | 4 | export default database.ref('/users/{uid}').onUpdate((change, context) => { 5 | const before = change.before.val(); 6 | const after = change.after.val(); 7 | const { isAdmin } = after; 8 | 9 | if (before.isAdmin === isAdmin) { 10 | return null; 11 | } 12 | 13 | const { uid } = context.params; 14 | 15 | return auth().setCustomUserClaims(uid, { 16 | isAdmin, 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /functions/src/firestore/users/onDelete.function.ts: -------------------------------------------------------------------------------- 1 | import { auth } from 'firebase-admin'; 2 | import { firestore } from 'firebase-functions'; 3 | 4 | export default firestore 5 | .document('users/{uid}') 6 | .onDelete((snapshot, context) => { 7 | const { uid } = context.params; 8 | return auth().deleteUser(uid); 9 | }); 10 | -------------------------------------------------------------------------------- /functions/src/firestore/users/onUpdate.function.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-functions'; 2 | import { auth } from 'firebase-admin'; 3 | 4 | export default firestore 5 | .document('/users/{uid}') 6 | .onUpdate((change, context) => { 7 | const before = change.before.data(); 8 | const after = change.after.data(); 9 | const { isAdmin } = after; 10 | 11 | if (before.isAdmin === isAdmin) { 12 | return null; 13 | } 14 | 15 | const { uid } = context.params; 16 | 17 | return auth().setCustomUserClaims(uid, { 18 | isAdmin, 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /functions/src/https/createUser.function.ts: -------------------------------------------------------------------------------- 1 | import { https } from 'firebase-functions'; 2 | import { auth } from 'firebase-admin'; 3 | 4 | const createUserAuth = async (email: string, isAdmin: boolean) => { 5 | const { uid } = await auth().createUser({ email }); 6 | 7 | await auth().setCustomUserClaims(uid, { 8 | isAdmin 9 | }); 10 | 11 | return uid; 12 | }; 13 | 14 | export default https.onCall(async data => { 15 | const { email, isAdmin } = data; 16 | 17 | if (!email) { 18 | throw new https.HttpsError('invalid-argument', 'auth/invalid-email'); 19 | } 20 | 21 | let uid; 22 | try { 23 | uid = await createUserAuth(email, isAdmin); 24 | } catch (error) { 25 | throw new https.HttpsError('invalid-argument', error.code); 26 | } 27 | 28 | return { uid }; 29 | }); 30 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as loadFunctions from 'firebase-function-tools'; 2 | import * as admin from 'firebase-admin'; 3 | // This import is needed by admin.initializeApp() to get the project info (Database url, project id, etc) 4 | // @ts-ignore 5 | import * as functions from 'firebase-functions'; 6 | 7 | admin.initializeApp(); 8 | 9 | loadFunctions(__dirname, exports, '.function.js'); 10 | -------------------------------------------------------------------------------- /functions/src/types/firebase-function-tools.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'firebase-function-tools'; 2 | -------------------------------------------------------------------------------- /functions/test/db/users/onDelete.test.ts: -------------------------------------------------------------------------------- 1 | import { admin, test } from '../../util/config'; 2 | import * as chai from 'chai'; 3 | import * as chaiAsPromised from 'chai-as-promised'; 4 | import onDelete from '../../../src/db/users/onDelete.function'; 5 | import 'mocha'; 6 | 7 | chai.use(chaiAsPromised); 8 | 9 | describe('onDelete Realtime Database', () => { 10 | let userRecord: any; 11 | 12 | it('should delete the user from the authentication section', async () => { 13 | userRecord = await admin.auth().createUser({ email: 'user@example.com' }); 14 | 15 | const wrapped = test.wrap(onDelete); 16 | 17 | await wrapped( 18 | {}, 19 | { 20 | params: { 21 | uid: userRecord.uid, 22 | }, 23 | } 24 | ); 25 | return await chai 26 | .expect(admin.auth().getUser(userRecord.uid)) 27 | .to.be.rejectedWith( 28 | Error, 29 | 'There is no user record corresponding to the provided identifier.' 30 | ); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /functions/test/db/users/onUpdate.test.ts: -------------------------------------------------------------------------------- 1 | import { admin, test } from '../../util/config'; 2 | import * as chai from 'chai'; 3 | import onUpdate from '../../../src/db/users/onUpdate.function'; 4 | import 'mocha'; 5 | 6 | describe('onUpdate Realtime Database', () => { 7 | let userRecord: any; 8 | 9 | before(async () => { 10 | const user = { 11 | email: 'user@example.com', 12 | password: 'secretPassword', 13 | }; 14 | const customClaims = { 15 | isAdmin: false, 16 | }; 17 | 18 | userRecord = await admin.auth().createUser(user); 19 | await admin.auth().setCustomUserClaims(userRecord.uid, customClaims); 20 | }); 21 | 22 | after(async () => { 23 | await admin.auth().deleteUser(userRecord.uid); 24 | }); 25 | 26 | it("should update user's custom claims in auth", async () => { 27 | const wrapped = test.wrap(onUpdate); 28 | 29 | const beforeSnap = test.database.makeDataSnapshot( 30 | { email: 'user@example.com', isAdmin: false }, 31 | `users/${userRecord.uid}` 32 | ); 33 | const afterSnap = test.database.makeDataSnapshot( 34 | { email: 'user@example.com', isAdmin: true }, 35 | `users/${userRecord.uid}` 36 | ); 37 | 38 | const change = test.makeChange(beforeSnap, afterSnap); 39 | 40 | await wrapped(change, { 41 | params: { 42 | uid: userRecord.uid, 43 | }, 44 | }); 45 | return admin 46 | .auth() 47 | .getUser(userRecord.uid) 48 | .then((snap) => { 49 | chai.assert.isTrue(snap.customClaims!.isAdmin); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /functions/test/firestore/users/onDelete.test.ts: -------------------------------------------------------------------------------- 1 | import { admin, test } from '../../util/config'; 2 | import * as chai from 'chai'; 3 | import * as chaiAsPromised from 'chai-as-promised'; 4 | import onDelete from '../../../src/firestore/users/onDelete.function'; 5 | import 'mocha'; 6 | 7 | chai.use(chaiAsPromised); 8 | 9 | describe('onDelete Firestore', () => { 10 | let userRecord: any; 11 | 12 | it('should delete the user from the authentication section', async () => { 13 | userRecord = await admin.auth().createUser({ email: 'user@example.com' }); 14 | 15 | const wrapped = test.wrap(onDelete); 16 | 17 | await wrapped( 18 | {}, 19 | { 20 | params: { 21 | uid: userRecord.uid, 22 | }, 23 | } 24 | ); 25 | return await chai 26 | .expect(admin.auth().getUser(userRecord.uid)) 27 | .to.be.rejectedWith( 28 | Error, 29 | 'There is no user record corresponding to the provided identifier.' 30 | ); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /functions/test/firestore/users/onUpdate.test.ts: -------------------------------------------------------------------------------- 1 | import { admin, test } from '../../util/config'; 2 | import * as chai from 'chai'; 3 | import onUpdate from '../../../src/firestore/users/onUpdate.function'; 4 | import 'mocha'; 5 | 6 | describe('onUpdate Firestore', () => { 7 | let userRecord: any; 8 | 9 | before(async () => { 10 | const user = { 11 | email: 'user@example.com', 12 | password: 'secretPassword', 13 | }; 14 | const customClaims = { 15 | isAdmin: false, 16 | }; 17 | 18 | userRecord = await admin.auth().createUser(user); 19 | await admin.auth().setCustomUserClaims(userRecord.uid, customClaims); 20 | }); 21 | 22 | after(async () => { 23 | await admin.auth().deleteUser(userRecord.uid); 24 | }); 25 | 26 | it("should update user's custom claims in auth", async () => { 27 | const wrapped = test.wrap(onUpdate); 28 | 29 | const beforeSnap = test.firestore.makeDocumentSnapshot( 30 | { email: 'user@example.com', isAdmin: false }, 31 | `users/${userRecord.uid}` 32 | ); 33 | const afterSnap = test.firestore.makeDocumentSnapshot( 34 | { email: 'user@example.com', isAdmin: true }, 35 | `users/${userRecord.uid}` 36 | ); 37 | 38 | const change = test.makeChange(beforeSnap, afterSnap); 39 | 40 | await wrapped(change, { 41 | params: { 42 | uid: userRecord.uid, 43 | }, 44 | }); 45 | return admin 46 | .auth() 47 | .getUser(userRecord.uid) 48 | .then((snap) => { 49 | chai.assert.isTrue(snap.customClaims!.isAdmin); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /functions/test/https/createUser.test.ts: -------------------------------------------------------------------------------- 1 | import { admin, test } from '../util/config'; 2 | import { https } from 'firebase-functions'; 3 | import * as chai from 'chai'; 4 | import * as chaiAsPromised from 'chai-as-promised'; 5 | import * as createUser from '../../src/https/createUser.function'; 6 | import 'mocha'; 7 | 8 | chai.use(chaiAsPromised); 9 | 10 | describe('createUser', () => { 11 | let userRecord: any; 12 | 13 | after(async () => { 14 | await admin.auth().deleteUser(userRecord.uid); 15 | }); 16 | 17 | it('should throw an error because the email is not provided', () => { 18 | const wrapped = test.wrap(createUser.default); 19 | 20 | const data = { 21 | email: '', 22 | isAdmin: false 23 | }; 24 | 25 | return chai 26 | .expect(wrapped(data)) 27 | .to.be.rejectedWith(https.HttpsError, 'auth/invalid-email'); 28 | }); 29 | 30 | it('should create the user in auth with correct email and custom claims', () => { 31 | const wrapped = test.wrap(createUser.default); 32 | 33 | const data = { 34 | email: 'user@example.com', 35 | isAdmin: false 36 | }; 37 | 38 | return wrapped(data).then(async (res: any) => { 39 | userRecord = await admin.auth().getUser(res.uid); 40 | 41 | chai.assert.equal(data.email, userRecord.email); 42 | chai.assert.equal(data.isAdmin, userRecord.customClaims!.isAdmin); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /functions/test/util/config.ts: -------------------------------------------------------------------------------- 1 | import * as testConfig from 'firebase-functions-test'; 2 | import * as admin from 'firebase-admin'; 3 | import { 4 | databaseURL, 5 | storageBucket, 6 | projectId, 7 | serviceAccountKey 8 | } from '../../env.json'; 9 | 10 | const projectConfig = { 11 | databaseURL, 12 | storageBucket, 13 | projectId, 14 | serviceAccountKey 15 | }; 16 | 17 | const test = testConfig(projectConfig, serviceAccountKey); 18 | 19 | admin.initializeApp(); 20 | 21 | export { admin, test }; 22 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017", 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true 12 | }, 13 | "compileOnSave": true, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /functions/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // -- Strict errors -- 4 | // These lint rules are likely always a good idea. 5 | 6 | // Force function overloads to be declared together. This ensures readers understand APIs. 7 | "adjacent-overload-signatures": true, 8 | 9 | // Do not allow the subtle/obscure comma operator. 10 | "ban-comma-operator": true, 11 | 12 | // Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules. 13 | "no-namespace": true, 14 | 15 | // Do not allow parameters to be reassigned. To avoid bugs, developers should instead assign new values to new vars. 16 | "no-parameter-reassignment": true, 17 | 18 | // Force the use of ES6-style imports instead of /// imports. 19 | "no-reference": true, 20 | 21 | // Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the 22 | // code currently being edited (they may be incorrectly handling a different type case that does not exist). 23 | "no-unnecessary-type-assertion": true, 24 | 25 | // Disallow nonsensical label usage. 26 | "label-position": true, 27 | 28 | // Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }. 29 | "no-conditional-assignment": true, 30 | 31 | // Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed). 32 | "no-construct": true, 33 | 34 | // Do not allow super() to be called twice in a constructor. 35 | "no-duplicate-super": true, 36 | 37 | // Do not allow the same case to appear more than once in a switch block. 38 | "no-duplicate-switch-case": true, 39 | 40 | // Do not allow a variable to be declared more than once in the same block. Consider function parameters in this 41 | // rule. 42 | "no-duplicate-variable": [true, "check-parameters"], 43 | 44 | // Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should 45 | // instead use a separate variable name. 46 | "no-shadowed-variable": true, 47 | 48 | // Empty blocks are almost never needed. Allow the one general exception: empty catch blocks. 49 | "no-empty": [true, "allow-empty-catch"], 50 | 51 | // Functions must either be handled directly (e.g. with a catch() handler) or returned to another function. 52 | // This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on. 53 | "no-floating-promises": true, 54 | 55 | // Do not allow any imports for modules that are not in package.json. These will almost certainly fail when 56 | // deployed. 57 | "no-implicit-dependencies": true, 58 | 59 | // The 'this' keyword can only be used inside of classes. 60 | "no-invalid-this": true, 61 | 62 | // Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead. 63 | "no-string-throw": true, 64 | 65 | // Disallow control flow statements, such as return, continue, break, and throw in finally blocks. 66 | "no-unsafe-finally": true, 67 | 68 | // Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid(); 69 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 70 | 71 | // Disallow duplicate imports in the same file. 72 | "no-duplicate-imports": true, 73 | 74 | 75 | // -- Strong Warnings -- 76 | // These rules should almost never be needed, but may be included due to legacy code. 77 | // They are left as a warning to avoid frustration with blocked deploys when the developer 78 | // understand the warning and wants to deploy anyway. 79 | 80 | // Warn when an empty interface is defined. These are generally not useful. 81 | "no-empty-interface": {"severity": "warning"}, 82 | 83 | // Warn when an import will have side effects. 84 | "no-import-side-effect": {"severity": "warning"}, 85 | 86 | // Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for 87 | // most values and let for values that will change. 88 | "no-var-keyword": {"severity": "warning"}, 89 | 90 | // Prefer === and !== over == and !=. The latter operators support overloads that are often accidental. 91 | "triple-equals": {"severity": "warning"}, 92 | 93 | // Warn when using deprecated APIs. 94 | "deprecation": {"severity": "warning"}, 95 | 96 | // -- Light Warnings -- 97 | // These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info" 98 | // if TSLint supported such a level. 99 | 100 | // prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array. 101 | // (Even better: check out utils like .map if transforming an array!) 102 | "prefer-for-of": {"severity": "warning"}, 103 | 104 | // Warns if function overloads could be unified into a single function with optional or rest parameters. 105 | "unified-signatures": {"severity": "warning"}, 106 | 107 | // Prefer const for values that will not change. This better documents code. 108 | "prefer-const": {"severity": "warning"}, 109 | 110 | // Multi-line object literals and function calls should have a trailing comma. This helps avoid merge conflicts. 111 | "trailing-comma": {"severity": "warning"} 112 | }, 113 | 114 | "defaultSeverity": "error" 115 | } 116 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-firebase-admin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@hookform/resolvers": "^0.1.0", 7 | "bulma": "^0.9.1", 8 | "classnames": "^2.2.6", 9 | "date-fns": "^2.16.1", 10 | "firebase": "^8.1.2", 11 | "mutationobserver-shim": "^0.3.7", 12 | "node-sass": "^4.14.1", 13 | "prop-types": "^15.7.2", 14 | "react": "^17.0.1", 15 | "react-datepicker": "^3.3.0", 16 | "react-dom": "^17.0.1", 17 | "react-firebaseui": "^4.1.0", 18 | "react-hook-form": "^6.12.2", 19 | "react-intl": "^5.10.6", 20 | "react-redux": "^7.2.2", 21 | "react-redux-toastr": "^7.6.5", 22 | "react-router-dom": "^5.2.0", 23 | "react-scripts": "4.0.1", 24 | "react-spinners": "^0.9.0", 25 | "react-table": "^7.6.2", 26 | "redux": "^4.0.5", 27 | "redux-act": "^1.8.0", 28 | "redux-persist": "^6.0.0", 29 | "redux-thunk": "^2.3.0", 30 | "yup": "^0.32.5" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject", 37 | "lint": "eslint .", 38 | "setup-admin-dashboard": "npm install && npm run build && firebase deploy", 39 | "deploy": "npm run build && firebase deploy --only hosting", 40 | "precommit:react": "npm test", 41 | "precommit:functions": "cd functions/ && npm run build && npm test", 42 | "precommit": "cross-env CI=true npm run precommit:react && npm run precommit:functions" 43 | }, 44 | "eslintConfig": { 45 | "extends": "react-app" 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "devDependencies": { 60 | "@testing-library/jest-dom": "^5.11.6", 61 | "@testing-library/react": "^11.2.2", 62 | "babel-eslint": "^10.1.0", 63 | "cross-env": "^7.0.3", 64 | "deep-freeze": "^0.0.1", 65 | "dotenv": "^8.2.0", 66 | "eslint": "^7.15.0", 67 | "eslint-config-airbnb": "^18.2.0", 68 | "eslint-config-prettier": "^6.11.0", 69 | "eslint-loader": "^4.0.2", 70 | "eslint-plugin-import": "^2.21.2", 71 | "eslint-plugin-jest": "^23.18.0", 72 | "eslint-plugin-jsx-a11y": "^6.3.0", 73 | "eslint-plugin-prettier": "^3.1.4", 74 | "eslint-plugin-promise": "^4.2.1", 75 | "eslint-plugin-react": "^7.20.5", 76 | "husky": "^4.3.5", 77 | "lint-staged": "^10.5.3", 78 | "prettier": "^2.2.1", 79 | "redux-mock-store": "^1.5.4", 80 | "sass-loader": "^10.1.0" 81 | }, 82 | "husky": { 83 | "hooks": { 84 | "pre-commit": "npm run precommit && lint-staged" 85 | } 86 | }, 87 | "jest": { 88 | "collectCoverageFrom": [ 89 | "src/**/*.{js,jsx}" 90 | ], 91 | "coverageReporters": [ 92 | "text", 93 | "html" 94 | ], 95 | "resetMocks": true 96 | }, 97 | "lint-staged": { 98 | "*.{js,jsx}": [ 99 | "eslint --fix", 100 | "prettier --write" 101 | ], 102 | "functions/**/*.ts": [ 103 | "npm run lint" 104 | ] 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 20 | 21 | 30 | React Firebase 31 | 32 | 33 | 39 | 43 | 44 | 45 | 46 |
47 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/assets/404.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreateThrive/react-firebase-admin/0464be8b1181e7411416d31715c33f3135a81e44/src/assets/404.gif -------------------------------------------------------------------------------- /src/assets/default-image-establishment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreateThrive/react-firebase-admin/0464be8b1181e7411416d31715c33f3135a81e44/src/assets/default-image-establishment.jpg -------------------------------------------------------------------------------- /src/assets/en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreateThrive/react-firebase-admin/0464be8b1181e7411416d31715c33f3135a81e44/src/assets/en.png -------------------------------------------------------------------------------- /src/assets/es.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreateThrive/react-firebase-admin/0464be8b1181e7411416d31715c33f3135a81e44/src/assets/es.png -------------------------------------------------------------------------------- /src/assets/user-default-log.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreateThrive/react-firebase-admin/0464be8b1181e7411416d31715c33f3135a81e44/src/components/.gitkeep -------------------------------------------------------------------------------- /src/components/ConfirmationModal/ConfirmationModal.scss: -------------------------------------------------------------------------------- 1 | header.modal-card-head, 2 | footer.modal-card-foot { 3 | border: 1px solid #f1f2f2; 4 | background: white; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/ConfirmationModal/ConfirmationModal.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent } from '@testing-library/react'; 3 | 4 | import ConfirmationModal from '.'; 5 | 6 | describe(' rendering', () => { 7 | const onConfirm = jest.fn(); 8 | const onCancel = jest.fn(); 9 | 10 | beforeEach(() => { 11 | onConfirm.mockClear(); 12 | onCancel.mockClear(); 13 | }); 14 | 15 | it('should render without crashing', () => { 16 | const component = render( 17 | 26 | ); 27 | 28 | expect(component.asFragment()).toMatchSnapshot(); 29 | }); 30 | 31 | it('should set the active modifier if the isActive prop is passed down', () => { 32 | const component = render( 33 | 42 | ); 43 | 44 | expect( 45 | component.container.querySelector('div.modal.is-active') 46 | ).toBeTruthy(); 47 | }); 48 | 49 | it('should not set the active modifier if the isActive prop is not passed down', () => { 50 | const component = render( 51 | 59 | ); 60 | 61 | expect(component.container.querySelector('div.modal.is-active')).toBeNull(); 62 | }); 63 | 64 | it('should call onConfirm when the confirmation button is clicked', () => { 65 | const component = render( 66 | 75 | ); 76 | 77 | fireEvent.click(component.getByText('confirm test message')); 78 | expect(onConfirm).toHaveBeenCalled(); 79 | }); 80 | 81 | it('should call onCancel when the cancel button is clicked', () => { 82 | const component = render( 83 | 92 | ); 93 | 94 | fireEvent.click(component.getByText('cancel test message')); 95 | 96 | expect(onCancel).toHaveBeenCalled(); 97 | }); 98 | 99 | it('should set the title of the modal', () => { 100 | const component = render( 101 | 110 | ); 111 | 112 | expect(component.getByText('test title')).toBeTruthy(); 113 | }); 114 | 115 | it('should set the body of the modal', () => { 116 | const component = render( 117 | 126 | ); 127 | 128 | expect(component.getByText('test body')).toBeTruthy(); 129 | }); 130 | 131 | it('should set the confirm button message', () => { 132 | const component = render( 133 | 142 | ); 143 | 144 | expect(component.getByText('confirm test message')).toBeTruthy(); 145 | }); 146 | 147 | it('should set the cancel button message', () => { 148 | const component = render( 149 | 158 | ); 159 | 160 | expect(component.getByText('cancel test message')).toBeTruthy(); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /src/components/ConfirmationModal/__snapshots__/ConfirmationModal.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` rendering should render without crashing 1`] = ` 4 | 5 |