Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/auth flow #125

Merged
merged 18 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Loading