Skip to content

Commit

Permalink
Feat/auth flow (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidgamez authored Oct 12, 2023
1 parent 556030a commit ed598c3
Show file tree
Hide file tree
Showing 37 changed files with 1,587 additions and 164 deletions.
59 changes: 49 additions & 10 deletions .github/workflows/web-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ jobs:
**/.eslintcache
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn-lock.json') }}
- name: Install dependencies
working-directory: web-app
Expand All @@ -51,7 +49,7 @@ jobs:
- name: Cypress test
uses: cypress-io/github-action@v6
with:
start: yarn start
start: yarn start:test
wait-on: 'npx wait-on --timeout 120000 http://127.0.0.1:3000'
working-directory: web-app

Expand Down Expand Up @@ -113,31 +111,52 @@ jobs:
**/.eslintcache
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn-lock.json') }}
- name: Install dependencies
working-directory: web-app
run: yarn install --frozen-lockfile

- name: Set Firebase project name
- name: Set Firebase project properties
working-directory: web-app
run: |
if [[ $GITHUB_EVENT_NAME == 'push' && $GITHUB_REF == 'refs/heads/main' ]]; then
echo "Setting FIREBASE_PROJECT to 'pushed to main branch'"
echo "FIREBASE_PROJECT=qa" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_API_KEY=${{ secrets.QA_REACT_APP_FIREBASE_API_KEY }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_AUTH_DOMAIN=${{ secrets.QA_REACT_APP_FIREBASE_AUTH_DOMAIN }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_PROJECT_ID=${{ secrets.QA_REACT_APP_FIREBASE_PROJECT_ID }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_STORAGE_BUCKET=${{ secrets.QA_REACT_APP_FIREBASE_STORAGE_BUCKET }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_MESSAGING_SENDER_ID=${{ secrets.QA_REACT_APP_FIREBASE_MESSAGING_SENDER_ID }}" >> $GITHUB_ENV
echo "REACT_REACT_APP_FIREBASE_APP_ID=${{ secrets.QA_REACT_APP_FIREBASE_APP_ID }}" >> $GITHUB_ENV
elif [[ $GITHUB_EVENT_NAME == 'release' ]]; then
echo "Setting FIREBASE_PROJECT to 'release'"
echo "FIREBASE_PROJECT=prod" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_API_KEY=${{ secrets.PROD_REACT_APP_FIREBASE_API_KEY }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_AUTH_DOMAIN=${{ secrets.PROD_REACT_APP_FIREBASE_AUTH_DOMAIN }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_PROJECT_ID=${{ secrets.PROD_REACT_APP_FIREBASE_PROJECT_ID }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_STORAGE_BUCKET=${{ secrets.PROD_REACT_APP_FIREBASE_STORAGE_BUCKET }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_MESSAGING_SENDER_ID=${{ secrets.PROD_REACT_APP_FIREBASE_MESSAGING_SENDER_ID }}" >> $GITHUB_ENV
echo "REACT_REACT_APP_FIREBASE_APP_ID=${{ secrets.PROD_REACT_APP_FIREBASE_APP_ID }}" >> $GITHUB_ENV
else
echo "Setting FIREBASE_PROJECT to 'dev'"
echo "FIREBASE_PROJECT=dev" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_API_KEY=${{ secrets.DEV_REACT_APP_FIREBASE_API_KEY }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_AUTH_DOMAIN=${{ secrets.DEV_REACT_APP_FIREBASE_AUTH_DOMAIN }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_PROJECT_ID=${{ secrets.DEV_REACT_APP_FIREBASE_PROJECT_ID }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_STORAGE_BUCKET=${{ secrets.DEV_REACT_APP_FIREBASE_STORAGE_BUCKET }}" >> $GITHUB_ENV
echo "REACT_APP_FIREBASE_MESSAGING_SENDER_ID=${{ secrets.DEV_REACT_APP_FIREBASE_MESSAGING_SENDER_ID }}" >> $GITHUB_ENV
echo "REACT_REACT_APP_FIREBASE_APP_ID=${{ secrets.DEV_REACT_APP_FIREBASE_APP_ID }}" >> $GITHUB_ENV
fi
- name: Populate Variables
working-directory: web-app
run: |
../scripts/replace-variables.sh -in_file src/.env.rename_me -out_file src/.env.${{ env.FIREBASE_PROJECT }} -variables REACT_APP_FIREBASE_API_KEY,REACT_APP_FIREBASE_AUTH_DOMAIN,REACT_APP_FIREBASE_PROJECT_ID,REACT_APP_FIREBASE_STORAGE_BUCKET,REACT_APP_FIREBASE_MESSAGING_SENDER_ID,REACT_REACT_APP_FIREBASE_APP_ID
- name: Build
working-directory: web-app
run: yarn build
run: yarn build:${FIREBASE_PROJECT}

- name: Select Firebase Project
working-directory: web-app
Expand All @@ -155,8 +174,28 @@ jobs:
env:
PR_ID: ${{ github.event.number }}

- name: Check for Existing Comment
id: check-comment
working-directory: web-app
if: ${{ github.event_name == 'pull_request' }}
run: |
HOSTING_URL=$(npx firebase hosting:channel:list | grep "pr-${{ env.PR_ID }}" | awk '{print $7}')
COMMENT="Preview Firebase Hosting URL: $HOSTING_URL"
COMMENTS=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/${{ github.repository }}/issues/${{ env.PR_ID }}/comments")
JQ_CHECK=`echo "$COMMENTS" | jq -r ".[] | select(.body == \"$COMMENT\")"`
if [ -z "$JQ_CHECK" ]; then
echo "Comment does not exist."
echo "comment_exists=false" >> $GITHUB_OUTPUT
else
echo "Comment already exists."
echo "comment_exists=true" >> $GITHUB_OUTPUT
fi
env:
PR_ID: ${{ github.event.number }}

- name: Comment on PR with Hosting URL (PR Preview)
if: ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }}
if: ${{ github.event_name == 'pull_request' && steps.check-comment.outputs.comment_exists == 'false' }}
working-directory: web-app
run: |
HOSTING_URL=$(npx firebase hosting:channel:list | grep "pr-${{ env.PR_ID }}" | awk '{print $7}')
Expand Down
1 change: 0 additions & 1 deletion web-app/.eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
build/
node_modules/
src/reportWebVitals.ts
4 changes: 3 additions & 1 deletion web-app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ yarn-error.log*

# Cypress
cypress/screenshots
cypress/videos
cypress/videos

.env.*
66 changes: 66 additions & 0 deletions web-app/cypress/e2e/signup.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { passwordValidatioError } from '../../src/app/types';

describe('Sign up screen', () => {
beforeEach(() => {
cy.visit('/sign-up');
});

it('should render components', () => {
cy.get('input[id="email"]').should('exist');
cy.get('input[id="password"]').should('exist');
cy.get('input[id="confirmPassword"]').should('exist');
cy.get('button[id="sign-up-button"]').should('exist');
});

it('should show the password error when password length is less than 12', () => {
cy.get('input[id="password"]')
.should('exist')
.type('short', { force: true });

cy.get('[data-testid=passwordError]')
.should('exist')
.contains(passwordValidatioError);
});

it('should show the password error when password do not contain lowercase', () => {
cy.get('input[id="password"]')
.should('exist')
.type('UPPERCASE_10_!', { force: true });

cy.get('[data-testid=passwordError]')
.should('exist')
.contains(passwordValidatioError);
});

it('should show the password error when password do not contain uppercase', () => {
cy.get('input[id="password"]')
.should('exist')
.type('lowercase_10_!', { force: true });

cy.get('[data-testid=passwordError]')
.should('exist')
.contains(passwordValidatioError);
});

it('should not show the password error when password is valid', () => {
cy.get('input[id="password"]')
.should('exist')
.type('UP_lowercase_10_!', { force: true });

cy.get('[data-testid=passwordError]').should('not.exist');
});

it('should show the password error when password do not match', () => {
cy.get('input[id="password"]')
.should('exist')
.type('UP_lowercase_10_!', { force: true });

cy.get('input[id="confirmPassword"]')
.should('exist')
.type('UP_lowercase_11_!', { force: true });

cy.get('[data-testid=confirmPasswordError]')
.should('exist')
.contains('Passwords do not match');
});
});
34 changes: 24 additions & 10 deletions web-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,36 @@
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.9",
"@mui/material": "^5.14.9",
"@types/jest": "^27.5.2",
"@types/material-ui": "^0.21.12",
"@types/node": "^16.18.50",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@reduxjs/toolkit": "^1.9.6",
"firebase": "^10.4.0",
"formik": "^2.4.5",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-loading-overlay-ts": "^2.0.2",
"react-redux": "^8.1.3",
"react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.2.3",
"typeface-muli": "^1.1.13",
"web-vitals": "^2.1.4"
"yup": "^1.3.2"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"scripts": {
"start": "DISABLE_ESLINT_PLUGIN=true react-scripts start",
"build": "CI=false react-scripts build",
"start:dev": "env-cmd -f src/.env.dev react-scripts start",
"start:test": "env-cmd -f src/.env.test react-scripts start",
"build:dev": "CI=false env-cmd -f src/.env.dev react-scripts build",
"build:qa": "CI=false env-cmd -f src/.env.qa react-scripts build",
"start:prod": "env-cmd -f src/.env.prod react-scripts start",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint 'src/app/**/*.{js,ts,tsx}'",
"lint:fix": "eslint 'src/app/**/*.{js,ts,tsx}' --fix",
"cypress-run": "cypress run",
"cypress-open": "cypress open"
"cypress:run": "cypress run",
"cypress:open": "cypress open"
},
"eslintConfig": {
"extends": [
Expand All @@ -53,14 +58,23 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@react-firebase/auth": "^0.2.10",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/cypress": "^1.1.3",
"@types/jest": "^27.5.2",
"@types/material-ui": "^0.21.12",
"@types/node": "^16.18.50",
"@types/react": "^18.2.25",
"@types/react-dom": "^18.2.7",
"@types/react-redux": "^7.1.27",
"@types/react-router-dom": "^5.3.3",
"@types/redux-saga": "^0.10.5",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"cypress": "^13.2.0",
"env-cmd": "^10.1.0",
"eslint": "^8.49.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-standard-with-typescript": "^39.0.0",
Expand Down
7 changes: 7 additions & 0 deletions web-app/src/.env.rename_me
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DISABLE_ESLINT_PLUGIN=true
REACT_APP_FIREBASE_API_KEY={{REACT_APP_FIREBASE_API_KEY}}
REACT_APP_FIREBASE_AUTH_DOMAIN={{REACT_APP_FIREBASE_AUTH_DOMAIN}}
REACT_APP_FIREBASE_PROJECT_ID={{REACT_APP_FIREBASE_PROJECT_ID}}
REACT_APP_FIREBASE_STORAGE_BUCKET={{REACT_APP_FIREBASE_STORAGE_BUCKET}}
REACT_APP_FIREBASE_MESSAGING_SENDER_ID={{REACT_APP_FIREBASE_MESSAGING_SENDER_ID}}
REACT_APP_FIREBASE_APP_ID={{REACT_APP_FIREBASE_APP_ID}}
6 changes: 6 additions & 0 deletions web-app/src/.env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DISABLE_ESLINT_PLUGIN=true
REACT_APP_FIREBASE_API_KEY="REACT_APP_FIREBASE_API_KEY"
REACT_APP_FIREBASE_PROJECT_ID="REACT_APP_FIREBASE_PROJECT_ID"
REACT_APP_FIREBASE_STORAGE_BUCKET="REACT_APP_FIREBASE_STORAGE_BUCKET"
REACT_APP_FIREBASE_MESSAGING_SENDER_ID="REACT_APP_FIREBASE_MESSAGING_SENDER_ID"
REACT_APP_FIREBASE_APP_ID="REACT_APP_FIREBASE_APP_ID"
23 changes: 13 additions & 10 deletions web-app/src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import './App.css';
import AppRouter from './router/Router';
import ContextProviders from './util-component/Context';
import Footer from './util-component/Footer';
import Header from './util-component/Header';
import ContextProviders from './components/Context';
import Footer from './components/Footer';
import Header from './components/Header';
import { BrowserRouter } from 'react-router-dom';
import AppSpinner from './components/AppSpinner';

function App(): React.ReactElement {
require('typeface-muli'); // Load font
return (
<div className='container'>
<BrowserRouter>
<ContextProviders>
<Header />
<AppRouter />
<Footer />
</ContextProviders>
</BrowserRouter>
<ContextProviders>
<AppSpinner>
<BrowserRouter>
<Header />
<AppRouter />
<Footer />
</BrowserRouter>
</AppSpinner>
</ContextProviders>
</div>
);
}
Expand Down
19 changes: 19 additions & 0 deletions web-app/src/app/components/AppSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';
import type ContextProviderProps from '../interface/ContextProviderProps';
import LoadingOverlay from 'react-loading-overlay-ts';
import { useSelector } from 'react-redux';
import { selectLoadingApp } from '../store/selectors';

/**
* This adds a spinner to the entire application
*/
const AppSpinner: React.FC<ContextProviderProps> = ({ children }) => {
const isActive = useSelector(selectLoadingApp);
return (
<LoadingOverlay active={isActive} spinner>
{children}
</LoadingOverlay>
);
};

export default AppSpinner;
40 changes: 40 additions & 0 deletions web-app/src/app/components/Context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { useEffect } from 'react';
import type ContextProviderProps from '../interface/ContextProviderProps';
import { Provider } from 'react-redux';
import { store } from '../store/store';
import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist';
import { useAppDispatch } from '../hooks';
import { resetProfileErrors } from '../store/profile-reducer';

const persistor = persistStore(store);
/**
* This component is used to wrap the entire application
*/
const AppContent: React.FC<ContextProviderProps> = ({ children }) => {
const dispatch = useAppDispatch();

useEffect(() => {
// This function will run when the component is first loaded
// Clean errros from previous session
dispatch(resetProfileErrors());
}, []);
return (
<PersistGate loading={null} persistor={persistor}>
{children}
</PersistGate>
);
};

/**
* This component is used to wrap the entire application adding the store provider and reseting the errors from previous session
*/
const ContextProviders: React.FC<ContextProviderProps> = ({ children }) => {
return (
<Provider store={store}>
<AppContent>{children}</AppContent>
</Provider>
);
};

export default ContextProviders;
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit ed598c3

Please sign in to comment.