Skip to content

Commit

Permalink
Merge pull request #55 from ciatph/dev
Browse files Browse the repository at this point in the history
v1.1.1
  • Loading branch information
ciatph authored Jun 29, 2022
2 parents 7eab122 + af41f53 commit cd0950b
Show file tree
Hide file tree
Showing 19 changed files with 273 additions and 50 deletions.
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ jobs:
- run: rm -r -f .netrc

docker-build-push:
name: Deploy to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
Expand Down
85 changes: 59 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A basic web app client in the **/client** directory will show basic API usage an
- [Installation and Usage Using Docker](#installation-and-usage-using-docker)
- [Docker for Localhost Development](#docker-for-localhost-development)
- [Docker for Production Deployment](#docker-for-production-deployment)
- [Pre-built Server Docker Image](#pre-built-server-docker-image)
- [References](#references)

## Requirements
Expand Down Expand Up @@ -43,19 +44,15 @@ A basic web app client in the **/client** directory will show basic API usage an
cd server
npm install
```
3. Set up the environment variables. Create a `.env` file inside the **/server** directory with reference to the `.env.example` file. Encode your own Firebase project settings on the following variables:
- `FIREBASE_SERVICE_ACC`
- The project's private key file contents, condensed into one line and minus all whitespace characters.
- The service account JSON file is generated from the Firebase project's **Project Settings** page, on
**Project Settings** -> **Service accounts** -> **Generate new private key**
- `FIREBASE_PRIVATE_KEY`
- The `private_key` entry from the service account JSON file
- > **NOTE:** Take note to make sure that the value starts and ends with a double-quote on WINDOWS OS localhost. Some systems may or may not require the double-quotes (i.e., Ubuntu running on heroku).
- `ALLOWED_ORIGINS`
- IP/domain origins in comma-separated values that are allowed to access the API
- Include `http://localhost:3000` by default to allow CORS access to the `/client` app.
- `EMAIL_WHITELIST`
- Comma-separated email addresses linked to Firebase Auth UserRecords that are not allowed to be deleted or updated (write-protected)
3. Set up the environment variables. Create a `.env` file inside the **/server** directory with reference to the `.env.example` file. Encode your own Firebase project settings on the following variables:

| Variable Name | Description |
| --- | --- |
|FIREBASE_SERVICE_ACC| The project's private key file contents, condensed into one line and minus all whitespace characters.<br><br>The service account JSON file is generated from the Firebase project's **Project Settings** page, on **Project Settings** -> **Service accounts** -> **Generate new private key**|
|FIREBASE_PRIVATE_KEY| The `private_key` entry from the service account JSON file.<br> <blockquote> **NOTE:** Take note to make sure that the value starts and ends with a double-quote on WINDOWS OS localhost. Some systems may or may not require the double-quotes (i.e., Ubuntu running on heroku).</blockquote> |
|ALLOWED_ORIGINS|IP/domain origins in comma-separated values that are allowed to access the API. Include `http://localhost:3000` by default to allow CORS access to the `/client` app.|
|EMAIL_WHITELIST| Comma-separated email addresses linked to Firebase Auth UserRecords that are not allowed to be deleted or updated (write-protected)<br><br>Default value is `superadmin@gmail.com`|
|ALLOW_CORS|Allow Cross-Origin Resource Sharing (CORS) on the API endpoints.<br><br>Default value is `1`. Setting to `0` will make all endpoints accept requests from all domains, including Postman.|

### client

Expand All @@ -64,12 +61,15 @@ A basic web app client in the **/client** directory will show basic API usage an
cd client
npm install
```
2. Replace `/client/utils/firebase/firebase.config.js` with your own Firebase project's web SDK setup configuration file.
2. Replace the `/client/utils/firebase/firebase.config.js` file with your own Firebase project's web SDK setup configuration file.
- You can find this file in a Firebase project's
**Project Settings** -> **General** -> **Web apps** (Add an app if needed) -> **SDK setup and configuration**
3. Create a `/client/.env` file from the `/client/.env.example` file.
- The `firebase.config.js` settings must match with the `FIREBASE_SERVICE_ACC` environment variable provided on **server - step # 3.**
- Replace `REACT_APP_BASE_URL` with the domain on which the CRUD API is running (default value is `http://localhost:3001/api` on localhost. See the [server](#server) set-up instructions for more information).
- The `firebase.config.js` settings must match with the `FIREBASE_SERVICE_ACC` environment variable defined on **server - step # 3.**
3. Create a `/client/.env` file from the `/client/.env.example` file. Replace the `REACT_APP_BASE_URL` variable with an appropriate value.

| Variable Name | Description |
| --- | --- |
|REACT_APP_BASE_URL|Domain on which the CRUD API is running.<br><br> Default value is `http://localhost:3001/api` on localhost. See the [server](#server) set-up instructions for more information.|
4. Run the app in development mode.
`npm start`
5. Launch the client app in:
Expand Down Expand Up @@ -152,32 +152,65 @@ We can use Docker to run dockerized **client** and **server** apps for local dev
- > **INFO:** Building the images for localhost development takes a while, around ~7min+.
4. Create and start the client and server containers.
`docker-compose -f docker-compose-dev.yml up`
5. Launch the dockerized (dev) client app on
5. Run a script in the container to create the default `superadmin@gmail.com` account, if it does not yet exist in the Firestore database.
`docker exec -it server-prod npm run seed`
6. Launch the dockerized (dev) client app on
`http://localhost:3000`
6. Launch the dockerized (dev) server app's API documentation on
7. Launch the dockerized (dev) server app's API documentation on
`http://localhost:3001/docs`
7. Edit source the codes in `/client/src` or `/server/src` as needed. Verify that hot reload is working on both the client and server apps.
8. Stop and remove containers, networks, images and volumes:
8. Edit source the codes in `/client/src` or `/server/src` as needed. Verify that hot reload is working on both the client and server apps.
9. Stop and remove containers, networks, images and volumes:
`docker-compose -f docker-compose-dev.yml down`

### Docker for Production Deployment

The following docker-compose commands build small client and server images targeted for creating optimized dockerized apps running on production servers. Hot reload is not available when editing source codes from `/client/src` or `/server/src`.
The following docker-compose commands build small client and server images targeted for creating optimized dockerized apps running on self-managed production servers. Hot reload is not available when editing source codes from `/client/src` or `/server/src`.

1. Install and set up the required environment variables as with the required variables on **Docker for Localhost Development**.
1. Install and set up the required **client** and **server** environment variables as with the required variables on [**Docker for Localhost Development**](#docker-for-localhost-development).
2. Build the client and server docker services for production deployment.
- `docker-compose -f docker-compose-prod.yml build`
3. At this point, we can opt to push the docker images to a docker registry of your choice. (Requires sign-in to the selected docker registry).
- `docker-compose -f docker-compose-prod.yml push`
4. Create and start the client and server containers.
`docker-compose -f docker-compose-prod.yml up`
5. Launch the dockerized (prod) client app on
5. Run a script in the container to create the default `superadmin@gmail.com` account, if it does not yet exist in the Firestore database.
`docker exec -it server-prod npm run seed`
6. Launch the dockerized (prod) client app on
`http://localhost:3000`
6. Launch the dockerized (prod) server app's API documentation on
7. Launch the dockerized (prod) server app's API documentation on
`http://localhost:3001/docs`
7. Stop and remove containers, networks, images and volumes:
8. Stop and remove containers, networks, images and volumes:
`docker-compose -f docker-compose-prod.yml down`

## Pre-built Server Docker Image

**firebase-users-admin**'s `server` component is available as a stand-alone docker image on Docker Hub with customizable environment variables (.env file).

1. Pull the (production) **/server** docker image from Docker Hub.
`docker pull ciatphdev/firebase-users-admin-server:v1.1.1`
2. Create a `.env` file.
- Read [**Installation - server #3**](#server) for more information.
- Replace the variables accordingly in the `.env` file.
```
ALLOWED_ORIGINS=http://localhost,http://localhost:3000,http://mywebsite.com,http://yourwebsite.com
FIREBASE_SERVICE_ACC=YOUR-FIREBASE-PROJ-SERVICE-ACCOUNT-JSON-CREDENTIALS-ONE-LINER-NO-SPACES
FIREBASE_PRIVATE_KEY=PRIVATE-KEY-FROM-FIREBASE-SERVICE-ACCOUNT-JSON-WITH-DOUBLE-QUOTES
EMAIL_WHITELIST=superadmin@gmail.com
ALLOW_CORS=1
```
3. Run the image.
```
docker run -it --rm \
--env-file .env
-p 3001:3001 \
ciatphdev/firebase-users-admin-server:v1.1.1
```
4. Run a script in the container to create the default `superadmin@gmail.com` account, if it does not yet exist in the Firestore database.
`docker exec -it server-prod npm run seed`
5. Launch the server API documentation on
`http://localhost:3001/docs`


## References

[[1]](https://docs.docker.com/compose/reference/) - docker compose commands
Expand Down
22 changes: 16 additions & 6 deletions client/nginx/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
# Minimal nginx configuration for running locally in containers
server {
listen 3000;

root /usr/share/nginx/html;
include /etc/nginx/mime.types;
index index.html index.html;

server_name localhost;
server_tokens off;

location /api {
proxy_pass http://server-prod:3001;
# Rewrite all React URLs/routes to index.html
location / {
try_files $uri $uri/ /index.html =404;
}

location / {
root /usr/share/nginx/html;
index index.html index.html
try_files $uri /index.html;
# Reverse proxy to the backend API server
# Requires the backend service running on a container named 'server-prod'
location /api {
proxy_pass http://server-prod:3001;
proxy_set_header Host $host;
}

# Other error pages
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
Expand Down
103 changes: 103 additions & 0 deletions client/nginx/nginx.full.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Full nginx configuration with SSL certificate for nginx running on host machine
# Requires a registered domain name, letsencrypt SSL certificates
# and local client/server apps (running in containers or manually installed on host)

server {
listen 80;
listen [::]:80;
server_name www.<YOUR.DOMAIN.COM.HERE>;
return 301 https://<YOUR.DOMAIN.COM.HERE>$request_uri;
}

server {
listen 80;
listen [::]:80;
server_name <YOUR.DOMAIN.COM.HERE>;
return 301 https://<YOUR.DOMAIN.COM.HERE>$request_uri;
}

server {
listen 443 ssl;
server_name www.<YOUR.DOMAIN.COM.HERE>;
ssl_certificate /etc/letsencrypt/live/<YOUR.DOMAIN.COM.HERE>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<YOUR.DOMAIN.COM.HERE>/privkey.pem;
return 301 https://<YOUR.DOMAIN.COM.HERE>$request_uri;
}

server {
listen 443 ssl http2;
listen [::]:443 ssl http2;

server_name <YOUR.DOMAIN.COM.HERE>;
server_tokens off;

# Available methods
add_header Allow 'GET, POST, PATCH, DELETE, HEAD' always;
add_header X-XSS-Protection '1; mode=block';

if ( $request_method !~ ^(GET|POST|PATCH|DELETE|HEAD)$ ) {
return 405;
}

ssl_certificate /etc/letsencrypt/live/<YOUR.DOMAIN.COM.HERE>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<YOUR.DOMAIN.COM.HERE>/privkey.pem;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
ssl_dhparam '/etc/pki/nginx/dhparams.pem';

add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains' always;

# gzip comppression settings
gzip on;
gzip_disable 'msie6';

gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 0;
gzip_types text/plain application/javascript text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype;

# Reverse proxy to the client website
# Requires the client service running on http://localhost:3000 (from a container or manually installed on host)
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;

# For websockets
proxy_http_version 1.1;
proxy_set_header Connection 'upgrade';
proxy_set_header Upgrade $http_upgrade;
proxy_read_timeout 600s;
}

# Reverse proxy to the backend API server
# Requires the backend service running on http://localhost:3001 (from a container or manually installed on host)
location /api {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;

# For websockets
proxy_http_version 1.1;
proxy_set_header Connection 'upgrade';
proxy_set_header Upgrade $http_upgrade;
proxy_read_timeout 600s;
}

# Other error pages
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
7 changes: 7 additions & 0 deletions client/src/components/404/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function NotFound () {
return (
<h2>Page not found</h2>
)
}

export default NotFound
10 changes: 10 additions & 0 deletions client/src/components/common/userform/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ function UserForm (props) {
onChange={onTextChange}
/>

<TextField
id='password'
label='Enter password'
variant='outlined'
size='small'
disabled={loadstatus.isLoading}
value={state.password}
onChange={onTextChange}
/>

<InputLabel sx={styles.formlabel} id='accountlevel-label'>Account Type</InputLabel>
<Select
labelId='accountlevel-label'
Expand Down
2 changes: 1 addition & 1 deletion client/src/containers/createuser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createUser } from '../../utils/service'
import UserForm from '../../components/common/userform'

const defaultState = {
email: '', displayname: '', account_level: '1', disabled: false, emailverified: false
email: '', displayname: '', password: '', account_level: '1', disabled: false, emailverified: false
}

const defaultLoadingState = {
Expand Down
2 changes: 1 addition & 1 deletion client/src/containers/updateuser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { updateUser } from '../../utils/service'
import UserForm from '../../components/common/userform'

const defaultState = {
email: '', displayname: '', account_level: '1', disabled: false, emailverified: false
email: '', displayname: '', password: '', account_level: '1', disabled: false, emailverified: false
}

const defaultLoadingState = {
Expand Down
6 changes: 6 additions & 0 deletions client/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import DashboardContainer from './containers/dashboard'
import LoginContainer from './containers/login'
import CreateUserContainer from './containers/createuser'
import UpdateUserContainer from './containers/updateuser'
import NotFound from './components/404'
import Home from './components/home'

const routes = [
Expand Down Expand Up @@ -29,6 +30,11 @@ const routes = [
path: '/',
isProtected: false,
component: Home
},
{
path: '*',
isProtected: false,
component: NotFound
}
]

Expand Down
12 changes: 8 additions & 4 deletions client/src/utils/service/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ export default class Service {
}

async createUser (user) {
const fields = ['email', 'displayname', 'account_level', 'disabled', 'emailverified']
const fields = ['email', 'displayname', 'password', 'account_level', 'disabled', 'emailverified']
const body = {}

fields.forEach((item) => {
if (user[item] !== undefined) {
if (user[item] !== undefined && user[item] !== '') {
body[item] = user[item]
} else {
throw new Error('Please check your input.')
}
})

Expand All @@ -65,12 +67,14 @@ export default class Service {
}

async updateUser (info) {
const fields = ['uid', 'email', 'displayname', 'disabled', 'emailverified', 'account_level']
const fields = ['uid', 'email', 'displayname', 'password', 'disabled', 'emailverified', 'account_level']
const body = {}

fields.forEach((item) => {
if (info[item.toLowerCase()] !== undefined) {
if (info[item.toLowerCase()] !== undefined && info[item.toLowerCase()] !== '') {
body[item] = info[item.toLowerCase()]
} else {
throw new Error('Please check your input.')
}
})

Expand Down
4 changes: 3 additions & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
version: "2.6.0"
version: "3"
services:
# Create React App (CRA) running on development mode
client-dev:
container_name: client-dev
image: ciatphdev/firebase-users-admin-client:dev
Expand All @@ -15,6 +16,7 @@ services:
ports:
- "3000:3000"

# Express app running in development mode with auto reload using nodemon
server-dev:
container_name: server-dev
image: ciatphdev/firebase-users-admin-server:dev
Expand Down
Loading

0 comments on commit cd0950b

Please sign in to comment.