Skip to content

Commit

Permalink
Use Firebase config; merge GCFs into one (#330)
Browse files Browse the repository at this point in the history
  • Loading branch information
koistya authored Mar 13, 2019
1 parent 166d732 commit de9d49b
Show file tree
Hide file tree
Showing 17 changed files with 146 additions and 96 deletions.
4 changes: 3 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

APP_NAME=React Starter Kit
APP_DESCRIPTION=Bootstrap new web application projects with React.js and GraphQL
APP_ORIGIN=https://reactstarter.com
APP_ORIGIN=http://localhost:3000
APP_ENV=local
APP_VERSION=latest

# Google Cloud & Firebase
# https://console.cloud.google.com/apis/credentials
Expand Down
33 changes: 20 additions & 13 deletions .env.production
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
# Application settings

APP_ORIGIN=https://example-prod.firebaseapp.com

# Google Cloud & Firebase
# https://console.cloud.google.com/apis/credentials
# https://console.firebase.google.com/project/_/settings/general/
# https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk

# GCP_PROJECT=example-prod
# GCP_BROWSER_KEY=AIzaSyAsuqpqt29-TIwBAu01Nbt5QnC3FIKO4A4
# GCP_SERVER_KEY=AIzaSyAsuqpqt29-TIwBAu01Nbt5QnC3FIKO4A4
# GCP_SERVICE_KEY={"type":"service_account","project_id":"example-prod","private_key_id":"...","private_key":"...","client_email":"...","client_id":"...","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"..."}
# Environment variables for the production build (NODE_ENV=production)
#
# IMPORTANT NODE:
# Do not include any API keys, secrets, passwords into this file in favor of
# using Firebase Config API or something similar. For example:
#
# $ firebase --project=example-prod functions:config:set \
# app.app_origin="https://example.com" \
# app.gcp_service_key="xxxxx" \
# app.gcp_server_key="xxxxx" \
# app.jwt_secret="xxxxx" \
# app.google_client_id="xxxxx" \
# app.google_client_secret="xxxxx" \
# app.facebook_app_id="xxxxx" \
# app.facebook_app_secret="xxxxx" \
# app.pgdatabase="xxxxx" \
# app.password="xxxxx"
#

# Authentication

Expand All @@ -31,6 +36,8 @@ PGUSER=<user>
PGDATABASE=<database>
PGPASSWORD=<password>
PGAPPNAME=rsk
PGSSLMODE=
PGDEBUG=false

# Analytics

Expand Down
4 changes: 2 additions & 2 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Application settings
# Environment variables for the test build (NODE_ENV=test)

APP_ORIGIN=https://example-test.firebaseapp.com
# APP_ORIGIN=http://localhost:3000

# Google Cloud & Firebase
# https://console.cloud.google.com/apis/credentials
Expand Down
5 changes: 0 additions & 5 deletions .firebaserc

This file was deleted.

50 changes: 28 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ Also, you need to be familiar with [HTML][html], [CSS][css], [JavaScript][js] ([
│ ├── common/ # Shared React components and HOCs
│ ├── icons/ # Icon components
│ ├── legal/ # Terms of Use, Privacy Policy, etc.
│ ├── misc/ # Other pages (about us, contacts, etc.)
│ ├── news/ # News section (example)
│ ├── pages/ # Static pages (landing, about, privacy, etc.)
│ ├── server/ # Server-side code (API, authentication, etc.)
│ │ ├── db/ # Database client
│ │ ├── story/ # Story related schema, queries, and mutations
Expand All @@ -68,16 +68,17 @@ Also, you need to be familiar with [HTML][html], [CSS][css], [JavaScript][js] ([
│ │ ├── schema.js # GraphQL schema
│ │ └── ssr.js # Server-side rendering, e.g. ReactDOMServer.renderToString(<App />)
│ ├── user/ # User pages (login, account settings, user profile, etc)
│ ├── utils/ # Utility functions
│ ├── createRelay.js # Relay factory method for browser envrironment
│ ├── hooks.js # React.js hooks and Context providers
│ ├── index.js # Client-side entry point, e.g. ReactDOM.render(<App />, container)
│ ├── router.js # Universal application router
│ ├── serviceWorker.js # Service worker helper methods
│ └── theme.js # Overrides for Material UI default styles
├── ssl/ # SSL certificates for connecting to Cloud SQL instance
├── .env # Environment variables
├── .env.local # Environment variables overrides for local development
├── .env.production # Environment variables overrides for PROD environment
├── .env.test # Environment variables overrides for TEST environment
├── .env # Environment variables for local development
├── .env.production # Environment variables for the production build
├── .env.test # Environment variables for the test build
├── graphql.schema # GraphQL schema (auto-generated, used by Relay)
└── package.json # The list of project dependencies + NPM scripts
```
Expand Down Expand Up @@ -114,12 +115,13 @@ $ yarn db-change # Create a new database migration file
$ yarn db-migrate # Migrate database to the latest version
$ yarn db-rollback # Rollback the latest migration
$ yarn db-backup --env=prod # Write database backup to backup.sql
$ yarn db-restore # Restore database backup from backup.sql
$ yarn db-restore --env=dev # Restore database from backup.sql
$ yarn db # Open PostgreSQL shell (for testing/debugging)
```

**Note**: Appending `--env=prod` or `--env=test` flags to any of the commands above will force it
to use database connection settings from `.env.production` or `.env.test` files.
**Note**: Appending `--env=prod` or `--env=test` flags to any of the commands above will load the
corresponding database settings for the selected deployment environment from
[Firebase Config API](https://firebase.google.com/docs/functions/config-env)

### How to Test

Expand All @@ -131,29 +133,32 @@ $ yarn test # Run unit tests. Or, `yarn test -- --watch`

### How to Deploy

1. Create a new **Google Cloud** project and **Cloud SQL** database.
2. Configure authentication in **Firebase** dashboard.
3. Set Google Cloud project ID in `package.json` file (see `scripts`).
4. Set API keys, secrets and other settings in `.env.production` file.
5. Migrate the database by running `yarn db-migrate --env=prod`.
6. Finally, deploy your application by running `yarn deploy-prod`.
```bash
$ yarn build # Build the in production mode (NODE_ENV=production)
$ yarn deploy-test # Deploy the app to TEST environment
$ yarn deploy-prod # Deploy the app to PROD environment
```

For more information refer to the [Deployment](https://github.com/kriasoft/react-firebase-starter/wiki/deployment)
guide in the project's Wiki.

### How to Update

If you keep the original Git history after cloning this repo, you can always fetch and merge
the recent updates back into your project by running:

```bash
git remote add react-firebase-starter https://github.com/kriasoft/react-firebase-starter.git
git checkout master
git fetch react-firebase-starter
git merge react-firebase-starter/master
yarn install
yarn relay
$ git remote add rsk https://github.com/kriasoft/react-firebase-starter.git
$ git checkout master
$ git fetch rsk
$ git merge rsk/master
$ yarn install
```

_NOTE: Try to merge as soon as the new changes land on the master branch in the upstream repository,
otherwise your project may differ too much from the base/upstream repo._
_NOTE: Try to merge as soon as the new changes land on the `master` branch in the upstream
repository, otherwise your project may differ too much from the base/upstream repo.
Alternatively, you can use a folder diff tool like [Beyond Compare][bc] for keeping your project
up to date with the base repository._

### How to Contribute

Expand Down Expand Up @@ -229,3 +234,4 @@ and [contributors](https://github.com/kriasoft/react-firebase-starter/graphs/con
[vcjs]: https://marketplace.visualstudio.com/items?itemName=mgmcdermott.vscode-language-babel
[watchman]: https://github.com/facebook/watchman
[postgres]: https://www.postgresql.org/
[bc]: https://www.scootersoftware.com/
19 changes: 1 addition & 18 deletions firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
".env.local",
".env.*.local",
".eslintrc",
".firebaserc",
".flowconfig",
".gitattributes",
".gitignore",
Expand All @@ -38,25 +37,9 @@
"hosting": {
"public": "build/public",
"rewrites": [
{
"source": "/login",
"function": "default"
},
{
"source": "/login/**",
"function": "login"
},
{
"source": "/graphql",
"function": "graphql"
},
{
"source": "/graphql/**",
"function": "graphql"
},
{
"source": "**",
"function": "default"
"function": "app"
}
],
"headers": [
Expand Down
27 changes: 25 additions & 2 deletions knexfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,34 @@
*/

const fs = require('fs');
const cp = require('child_process');
const dotenv = require('dotenv');
const { env } = require('minimist')(process.argv.slice(2));

dotenv.config({ path: `.env.${env === 'prod' ? 'production' : env}` });
dotenv.config({ path: '.env.local' });
// Load API keys, secrets etc. from Firebase environment
// https://firebase.google.com/docs/functions/config-env
if (env && env !== 'dev') {
const { status, stdout } = cp.spawnSync(
'firebase',
[`--project=example-${env}`, 'functions:config:get'],
{ stdio: ['pipe', 'pipe', 'inherit'] },
);

if (status !== 0) process.exit(status);

const config = JSON.parse(stdout.toString()).app;
Object.keys(config).forEach(key => {
process.env[key.toUpperCase()] =
typeof key === 'object' ? JSON.stringify(config[key]) : config[key];
});

dotenv.config({ path: `.env.${process.env.NODE_ENV}` });
// delete process.env.PGHOST;
// delete process.env.PGSSLMODE;
} else {
dotenv.config({ path: '.env.local' });
}

dotenv.config({ path: '.env' });

// Knex configuration
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"cookie": "^0.3.1",
"cookie-parser": "^1.4.4",
"dataloader": "^1.4.0",
"dotenv": "^6.2.0",
"dotenv": "^7.0.0",
"ejs": "^2.6.1",
"express": "^4.16.4",
"express-graphql": "^0.7.1",
Expand Down
3 changes: 2 additions & 1 deletion scripts/db-backup.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ cmd = cp
.spawn(
'pg_dump',
[
'--verbose',
'--data-only',
'--no-owner',
'--no-privileges',
'--column-inserts',
// '--column-inserts',
'--disable-triggers',
'--exclude-table=migrations',
'--exclude-table=migrations_lock',
Expand Down
10 changes: 6 additions & 4 deletions src/server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,18 @@ if (process.env.NODE_ENV !== 'production') {
);
}

router.get('/graphql/model', (req, res) => {
res.send(templates.dataModel());
});
if (process.env.APP_ENV !== 'production') {
router.get('/graphql/model', (req, res) => {
res.send(templates.dataModel());
});
}

router.use(
'/graphql',
expressGraphQL(req => ({
schema,
context: new Context(req),
graphiql: true, // process.env.GCP_PROJECT !== '<name>',
graphiql: process.env.APP_ENV !== 'production',
pretty: false,
formatError: err => {
console.error(err.originalError || err);
Expand Down
25 changes: 25 additions & 0 deletions src/server/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* React Starter Kit for Firebase
* https://github.com/kriasoft/react-firebase-starter
* Copyright (c) 2015-present Kriasoft | MIT License
*/

/* @flow */

import { Router } from 'express';

import passport from './passport';
import login from './login';
import api from './api';
import ssr from './ssr';

const router = new Router();

router.use(passport.initialize());
router.use(passport.session());

router.use(login);
router.use(api);
router.use(ssr);

export default router;
4 changes: 3 additions & 1 deletion src/server/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export default {
// https://firebase.google.com/docs/web/setup
firebase: {
projectId: process.env.GCP_PROJECT,
authDomain: process.env.APP_ORIGIN.replace(/^https?:\/\//, ''),
authDomain: process.env.APP_ORIGIN.startsWith('http://localhost')
? `${process.env.GCP_PROJECT}.firebaseapp.com`
: process.env.APP_ORIGIN.replace(/^https?:\/\//, ''),
apiKey: process.env.GCP_BROWSER_KEY,
},

Expand Down
11 changes: 11 additions & 0 deletions src/server/db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,20 @@
import fs from 'fs';
import knex from 'knex';

// Make it easier to identify open database connections by running:
// SELECT * from pg_stat_activity;
if (process.env.X_GOOGLE_GCLOUD_PROJECT) {
process.env.PGAPPNAME = [
process.env.X_GOOGLE_GCLOUD_PROJECT,
process.env.X_GOOGLE_FUNCTION_NAME,
process.env.X_GOOGLE_FUNCTION_VERSION,
].join('/');
}

const db = knex({
client: 'pg',
connection: {
min: process.env.X_GOOGLE_FUNCTION_NAME === 'app' ? 1 : 0,
// Database connection pool must be set to max 1
// when running in serverless environment.
max: 1,
Expand Down
31 changes: 15 additions & 16 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ const express = require('express');
const firebase = require('firebase-admin');
const functions = require('firebase-functions');

// Infer runtime environment from the project's name, for example:
// "example-prod" => "prod"
// "example-test" => "test"
const [, env] = (x => x && x.match(/-(\w+)$/))(process.env.GCP_PROJECT) || [];
// Load API keys, secrets etc. from Firebase environment
// https://firebase.google.com/docs/functions/config-env
if (process.env.NODE_ENV === 'production') {
const { app: config } = functions.config();
Object.keys(config).forEach(key => {
process.env[key.toUpperCase()] =
typeof config[key] === 'object'
? JSON.stringify(config[key])
: config[key];
});
}

dotenv.config({ path: `.env.${env === 'prod' ? 'production' : env}` });
dotenv.config({ path: `.env.${process.env.NODE_ENV}` });
dotenv.config({ path: '.env.local' });
dotenv.config({ path: '.env' });

Expand All @@ -32,22 +39,14 @@ if (!firebase.apps.length) {

if (process.env.NODE_ENV === 'production') {
// Server environment
exports.login = functions
.runWith({ memory: '2GB' })
.https.onRequest(require('./login').default);
exports.graphql = functions
.runWith({ memory: '2GB' })
.https.onRequest(require('./api').default);
exports.default = functions
exports.app = functions
.runWith({ memory: '2GB' })
.https.onRequest(require('./ssr').default);
.https.onRequest(require('./app').default);
} else {
// Local/dev environment
const app = express();
const db = require('./db').default;
app.use(require('./login').default);
app.use(require('./api').default);
app.use(require('./ssr').default);
app.use(require('./app').default);
module.exports.default = app;
module.exports.dispose = () => db.destroy();
}
Loading

0 comments on commit de9d49b

Please sign in to comment.