Skip to content

Commit cd0950b

Browse files
authored
Merge pull request #55 from ciatph/dev
v1.1.1
2 parents 7eab122 + af41f53 commit cd0950b

File tree

19 files changed

+273
-50
lines changed

19 files changed

+273
-50
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ jobs:
101101
- run: rm -r -f .netrc
102102

103103
docker-build-push:
104+
name: Deploy to Docker Hub
104105
runs-on: ubuntu-latest
105106
steps:
106107
- name: Login to Docker Hub

README.md

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ A basic web app client in the **/client** directory will show basic API usage an
1313
- [Installation and Usage Using Docker](#installation-and-usage-using-docker)
1414
- [Docker for Localhost Development](#docker-for-localhost-development)
1515
- [Docker for Production Deployment](#docker-for-production-deployment)
16+
- [Pre-built Server Docker Image](#pre-built-server-docker-image)
1617
- [References](#references)
1718

1819
## Requirements
@@ -43,19 +44,15 @@ A basic web app client in the **/client** directory will show basic API usage an
4344
cd server
4445
npm install
4546
```
46-
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:
47-
- `FIREBASE_SERVICE_ACC`
48-
- The project's private key file contents, condensed into one line and minus all whitespace characters.
49-
- The service account JSON file is generated from the Firebase project's **Project Settings** page, on
50-
**Project Settings** -> **Service accounts** -> **Generate new private key**
51-
- `FIREBASE_PRIVATE_KEY`
52-
- The `private_key` entry from the service account JSON file
53-
- > **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).
54-
- `ALLOWED_ORIGINS`
55-
- IP/domain origins in comma-separated values that are allowed to access the API
56-
- Include `http://localhost:3000` by default to allow CORS access to the `/client` app.
57-
- `EMAIL_WHITELIST`
58-
- Comma-separated email addresses linked to Firebase Auth UserRecords that are not allowed to be deleted or updated (write-protected)
47+
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:
48+
49+
| Variable Name | Description |
50+
| --- | --- |
51+
|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**|
52+
|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> |
53+
|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.|
54+
|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 `[email protected]`|
55+
|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.|
5956

6057
### client
6158

@@ -64,12 +61,15 @@ A basic web app client in the **/client** directory will show basic API usage an
6461
cd client
6562
npm install
6663
```
67-
2. Replace `/client/utils/firebase/firebase.config.js` with your own Firebase project's web SDK setup configuration file.
64+
2. Replace the `/client/utils/firebase/firebase.config.js` file with your own Firebase project's web SDK setup configuration file.
6865
- You can find this file in a Firebase project's
6966
**Project Settings** -> **General** -> **Web apps** (Add an app if needed) -> **SDK setup and configuration**
70-
3. Create a `/client/.env` file from the `/client/.env.example` file.
71-
- The `firebase.config.js` settings must match with the `FIREBASE_SERVICE_ACC` environment variable provided on **server - step # 3.**
72-
- 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).
67+
- The `firebase.config.js` settings must match with the `FIREBASE_SERVICE_ACC` environment variable defined on **server - step # 3.**
68+
3. Create a `/client/.env` file from the `/client/.env.example` file. Replace the `REACT_APP_BASE_URL` variable with an appropriate value.
69+
70+
| Variable Name | Description |
71+
| --- | --- |
72+
|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.|
7373
4. Run the app in development mode.
7474
`npm start`
7575
5. Launch the client app in:
@@ -152,32 +152,65 @@ We can use Docker to run dockerized **client** and **server** apps for local dev
152152
- > **INFO:** Building the images for localhost development takes a while, around ~7min+.
153153
4. Create and start the client and server containers.
154154
`docker-compose -f docker-compose-dev.yml up`
155-
5. Launch the dockerized (dev) client app on
155+
5. Run a script in the container to create the default `[email protected]` account, if it does not yet exist in the Firestore database.
156+
`docker exec -it server-prod npm run seed`
157+
6. Launch the dockerized (dev) client app on
156158
`http://localhost:3000`
157-
6. Launch the dockerized (dev) server app's API documentation on
159+
7. Launch the dockerized (dev) server app's API documentation on
158160
`http://localhost:3001/docs`
159-
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.
160-
8. Stop and remove containers, networks, images and volumes:
161+
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.
162+
9. Stop and remove containers, networks, images and volumes:
161163
`docker-compose -f docker-compose-dev.yml down`
162164

163165
### Docker for Production Deployment
164166

165-
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`.
167+
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`.
166168

167-
1. Install and set up the required environment variables as with the required variables on **Docker for Localhost Development**.
169+
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).
168170
2. Build the client and server docker services for production deployment.
169171
- `docker-compose -f docker-compose-prod.yml build`
170172
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).
171173
- `docker-compose -f docker-compose-prod.yml push`
172174
4. Create and start the client and server containers.
173175
`docker-compose -f docker-compose-prod.yml up`
174-
5. Launch the dockerized (prod) client app on
176+
5. Run a script in the container to create the default `[email protected]` account, if it does not yet exist in the Firestore database.
177+
`docker exec -it server-prod npm run seed`
178+
6. Launch the dockerized (prod) client app on
175179
`http://localhost:3000`
176-
6. Launch the dockerized (prod) server app's API documentation on
180+
7. Launch the dockerized (prod) server app's API documentation on
177181
`http://localhost:3001/docs`
178-
7. Stop and remove containers, networks, images and volumes:
182+
8. Stop and remove containers, networks, images and volumes:
179183
`docker-compose -f docker-compose-prod.yml down`
180184

185+
## Pre-built Server Docker Image
186+
187+
**firebase-users-admin**'s `server` component is available as a stand-alone docker image on Docker Hub with customizable environment variables (.env file).
188+
189+
1. Pull the (production) **/server** docker image from Docker Hub.
190+
`docker pull ciatphdev/firebase-users-admin-server:v1.1.1`
191+
2. Create a `.env` file.
192+
- Read [**Installation - server #3**](#server) for more information.
193+
- Replace the variables accordingly in the `.env` file.
194+
```
195+
ALLOWED_ORIGINS=http://localhost,http://localhost:3000,http://mywebsite.com,http://yourwebsite.com
196+
FIREBASE_SERVICE_ACC=YOUR-FIREBASE-PROJ-SERVICE-ACCOUNT-JSON-CREDENTIALS-ONE-LINER-NO-SPACES
197+
FIREBASE_PRIVATE_KEY=PRIVATE-KEY-FROM-FIREBASE-SERVICE-ACCOUNT-JSON-WITH-DOUBLE-QUOTES
198+
199+
ALLOW_CORS=1
200+
```
201+
3. Run the image.
202+
```
203+
docker run -it --rm \
204+
--env-file .env
205+
-p 3001:3001 \
206+
ciatphdev/firebase-users-admin-server:v1.1.1
207+
```
208+
4. Run a script in the container to create the default `[email protected]` account, if it does not yet exist in the Firestore database.
209+
`docker exec -it server-prod npm run seed`
210+
5. Launch the server API documentation on
211+
`http://localhost:3001/docs`
212+
213+
181214
## References
182215
183216
[[1]](https://docs.docker.com/compose/reference/) - docker compose commands

client/nginx/nginx.conf

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1+
# Minimal nginx configuration for running locally in containers
12
server {
23
listen 3000;
4+
5+
root /usr/share/nginx/html;
6+
include /etc/nginx/mime.types;
7+
index index.html index.html;
8+
39
server_name localhost;
10+
server_tokens off;
411

5-
location /api {
6-
proxy_pass http://server-prod:3001;
12+
# Rewrite all React URLs/routes to index.html
13+
location / {
14+
try_files $uri $uri/ /index.html =404;
715
}
816

9-
location / {
10-
root /usr/share/nginx/html;
11-
index index.html index.html
12-
try_files $uri /index.html;
17+
# Reverse proxy to the backend API server
18+
# Requires the backend service running on a container named 'server-prod'
19+
location /api {
20+
proxy_pass http://server-prod:3001;
21+
proxy_set_header Host $host;
1322
}
1423

24+
# Other error pages
1525
error_page 500 502 503 504 /50x.html;
1626
location = /50x.html {
1727
root /usr/share/nginx/html;

client/nginx/nginx.full.conf

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Full nginx configuration with SSL certificate for nginx running on host machine
2+
# Requires a registered domain name, letsencrypt SSL certificates
3+
# and local client/server apps (running in containers or manually installed on host)
4+
5+
server {
6+
listen 80;
7+
listen [::]:80;
8+
server_name www.<YOUR.DOMAIN.COM.HERE>;
9+
return 301 https://<YOUR.DOMAIN.COM.HERE>$request_uri;
10+
}
11+
12+
server {
13+
listen 80;
14+
listen [::]:80;
15+
server_name <YOUR.DOMAIN.COM.HERE>;
16+
return 301 https://<YOUR.DOMAIN.COM.HERE>$request_uri;
17+
}
18+
19+
server {
20+
listen 443 ssl;
21+
server_name www.<YOUR.DOMAIN.COM.HERE>;
22+
ssl_certificate /etc/letsencrypt/live/<YOUR.DOMAIN.COM.HERE>/fullchain.pem;
23+
ssl_certificate_key /etc/letsencrypt/live/<YOUR.DOMAIN.COM.HERE>/privkey.pem;
24+
return 301 https://<YOUR.DOMAIN.COM.HERE>$request_uri;
25+
}
26+
27+
server {
28+
listen 443 ssl http2;
29+
listen [::]:443 ssl http2;
30+
31+
server_name <YOUR.DOMAIN.COM.HERE>;
32+
server_tokens off;
33+
34+
# Available methods
35+
add_header Allow 'GET, POST, PATCH, DELETE, HEAD' always;
36+
add_header X-XSS-Protection '1; mode=block';
37+
38+
if ( $request_method !~ ^(GET|POST|PATCH|DELETE|HEAD)$ ) {
39+
return 405;
40+
}
41+
42+
ssl_certificate /etc/letsencrypt/live/<YOUR.DOMAIN.COM.HERE>/fullchain.pem;
43+
ssl_certificate_key /etc/letsencrypt/live/<YOUR.DOMAIN.COM.HERE>/privkey.pem;
44+
45+
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
46+
ssl_prefer_server_ciphers on;
47+
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
48+
ssl_dhparam '/etc/pki/nginx/dhparams.pem';
49+
50+
add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains' always;
51+
52+
# gzip comppression settings
53+
gzip on;
54+
gzip_disable 'msie6';
55+
56+
gzip_vary on;
57+
gzip_proxied any;
58+
gzip_comp_level 6;
59+
gzip_buffers 16 8k;
60+
gzip_http_version 1.1;
61+
gzip_min_length 0;
62+
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;
63+
64+
# Reverse proxy to the client website
65+
# Requires the client service running on http://localhost:3000 (from a container or manually installed on host)
66+
location / {
67+
proxy_pass http://localhost:3000;
68+
proxy_set_header Host $host;
69+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
70+
proxy_set_header X-Forwarded-Proto $scheme;
71+
proxy_set_header X-Real-IP $remote_addr;
72+
proxy_cache_bypass $http_upgrade;
73+
74+
# For websockets
75+
proxy_http_version 1.1;
76+
proxy_set_header Connection 'upgrade';
77+
proxy_set_header Upgrade $http_upgrade;
78+
proxy_read_timeout 600s;
79+
}
80+
81+
# Reverse proxy to the backend API server
82+
# Requires the backend service running on http://localhost:3001 (from a container or manually installed on host)
83+
location /api {
84+
proxy_pass http://localhost:3001;
85+
proxy_set_header Host $host;
86+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
87+
proxy_set_header X-Forwarded-Proto $scheme;
88+
proxy_set_header X-Real-IP $remote_addr;
89+
proxy_cache_bypass $http_upgrade;
90+
91+
# For websockets
92+
proxy_http_version 1.1;
93+
proxy_set_header Connection 'upgrade';
94+
proxy_set_header Upgrade $http_upgrade;
95+
proxy_read_timeout 600s;
96+
}
97+
98+
# Other error pages
99+
error_page 500 502 503 504 /50x.html;
100+
location = /50x.html {
101+
root /usr/share/nginx/html;
102+
}
103+
}

client/src/components/404/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
function NotFound () {
2+
return (
3+
<h2>Page not found</h2>
4+
)
5+
}
6+
7+
export default NotFound

client/src/components/common/userform/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ function UserForm (props) {
5151
onChange={onTextChange}
5252
/>
5353

54+
<TextField
55+
id='password'
56+
label='Enter password'
57+
variant='outlined'
58+
size='small'
59+
disabled={loadstatus.isLoading}
60+
value={state.password}
61+
onChange={onTextChange}
62+
/>
63+
5464
<InputLabel sx={styles.formlabel} id='accountlevel-label'>Account Type</InputLabel>
5565
<Select
5666
labelId='accountlevel-label'

client/src/containers/createuser/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createUser } from '../../utils/service'
33
import UserForm from '../../components/common/userform'
44

55
const defaultState = {
6-
email: '', displayname: '', account_level: '1', disabled: false, emailverified: false
6+
email: '', displayname: '', password: '', account_level: '1', disabled: false, emailverified: false
77
}
88

99
const defaultLoadingState = {

client/src/containers/updateuser/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { updateUser } from '../../utils/service'
44
import UserForm from '../../components/common/userform'
55

66
const defaultState = {
7-
email: '', displayname: '', account_level: '1', disabled: false, emailverified: false
7+
email: '', displayname: '', password: '', account_level: '1', disabled: false, emailverified: false
88
}
99

1010
const defaultLoadingState = {

client/src/routes.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import DashboardContainer from './containers/dashboard'
22
import LoginContainer from './containers/login'
33
import CreateUserContainer from './containers/createuser'
44
import UpdateUserContainer from './containers/updateuser'
5+
import NotFound from './components/404'
56
import Home from './components/home'
67

78
const routes = [
@@ -29,6 +30,11 @@ const routes = [
2930
path: '/',
3031
isProtected: false,
3132
component: Home
33+
},
34+
{
35+
path: '*',
36+
isProtected: false,
37+
component: NotFound
3238
}
3339
]
3440

client/src/utils/service/service.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ export default class Service {
5050
}
5151

5252
async createUser (user) {
53-
const fields = ['email', 'displayname', 'account_level', 'disabled', 'emailverified']
53+
const fields = ['email', 'displayname', 'password', 'account_level', 'disabled', 'emailverified']
5454
const body = {}
5555

5656
fields.forEach((item) => {
57-
if (user[item] !== undefined) {
57+
if (user[item] !== undefined && user[item] !== '') {
5858
body[item] = user[item]
59+
} else {
60+
throw new Error('Please check your input.')
5961
}
6062
})
6163

@@ -65,12 +67,14 @@ export default class Service {
6567
}
6668

6769
async updateUser (info) {
68-
const fields = ['uid', 'email', 'displayname', 'disabled', 'emailverified', 'account_level']
70+
const fields = ['uid', 'email', 'displayname', 'password', 'disabled', 'emailverified', 'account_level']
6971
const body = {}
7072

7173
fields.forEach((item) => {
72-
if (info[item.toLowerCase()] !== undefined) {
74+
if (info[item.toLowerCase()] !== undefined && info[item.toLowerCase()] !== '') {
7375
body[item] = info[item.toLowerCase()]
76+
} else {
77+
throw new Error('Please check your input.')
7478
}
7579
})
7680

0 commit comments

Comments
 (0)