├── .babelrc ├── .circleci ├── Dockerfile └── config.yml ├── .editorconfig ├── .env ├── .env.production ├── .env.test ├── .eslintrc ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── launch.json ├── settings.json └── snippets │ └── javascript.json ├── LICENSE.txt ├── README.md ├── firebase.json ├── knexfile.js ├── migrations └── 20180101000000_initial.js ├── package.json ├── public ├── favicon.ico └── manifest.json ├── schema.graphql ├── scripts ├── db-backup.js ├── db-restore.js ├── db.js ├── post-deploy.js ├── pre-deploy.js ├── psql.js ├── setup.js └── update-schema.js ├── seeds └── seed.js ├── src ├── admin │ ├── AdminLayout.js │ ├── AdminStoryList.js │ ├── AdminUserList.js │ └── index.js ├── common │ ├── App.js │ ├── App.test.js │ ├── AppBar.js │ ├── AutoUpdater.js │ ├── CustomerChat.js │ ├── ErrorPage.js │ ├── Layout.js │ ├── LayoutFooter.js │ ├── Link.js │ ├── LoginButton.js │ ├── LoginDialog.js │ ├── LoginForm.js │ ├── TextField.js │ ├── UserMenu.js │ └── UserSettingsDialog.js ├── hooks │ ├── index.js │ ├── useAuth.js │ ├── useConfig.js │ ├── useFacebook.js │ ├── useFirebase.js │ ├── useGoogleMaps.js │ ├── useHistory.js │ ├── useRelay.js │ └── useReset.js ├── icons │ ├── Facebook.js │ ├── GitHub.js │ ├── Google.js │ ├── Instagram.js │ ├── Logout.js │ ├── Settings.js │ └── Twitter.js ├── index.js ├── landing │ ├── Home.js │ ├── HomeHero.js │ ├── HomeSponsors.js │ ├── HomeStack.js │ └── index.js ├── legal │ ├── Privacy.js │ ├── Terms.js │ └── index.js ├── misc │ ├── About.js │ └── index.js ├── mutations │ ├── DeleteUser.js │ ├── LikeStory.js │ ├── UpdateUser.js │ └── UpsertStory.js ├── news │ ├── News.js │ ├── Story.js │ ├── SubmitDialog.js │ └── index.js ├── relay.js ├── router.js ├── server │ ├── api.js │ ├── app.js │ ├── config.js │ ├── context.js │ ├── db.js │ ├── errors.js │ ├── index.js │ ├── login.js │ ├── mutations │ │ ├── README.md │ │ ├── index.js │ │ ├── story.js │ │ └── user.js │ ├── node.js │ ├── passport.js │ ├── queries │ │ ├── README.md │ │ ├── index.js │ │ ├── story.js │ │ └── user.js │ ├── relay.js │ ├── schema.js │ ├── ssr.js │ ├── templates │ │ ├── data-model.ejs │ │ ├── error.ejs │ │ ├── index.js │ │ └── ok.ejs │ ├── types │ │ ├── README.md │ │ ├── comment.js │ │ ├── identity.js │ │ ├── index.js │ │ ├── story.js │ │ └── user.js │ ├── utils │ │ ├── fields.js │ │ ├── index.js │ │ ├── map.js │ │ ├── relay.js │ │ ├── type.js │ │ ├── user.js │ │ └── username.js │ └── validator.js ├── serviceWorker.js ├── theme.js ├── user │ ├── Account.js │ ├── Login.js │ ├── UserProfile.js │ └── index.js └── utils │ ├── env.js │ ├── gtag.js │ ├── index.js │ ├── loading.js │ ├── openWindow.js │ └── scrolling.js ├── ssl └── README.md ├── storage.rules └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app-tools/config/babel"], 3 | "plugins": [ 4 | ["babel-plugin-lodash", { "id": ["lodash", "recompose"] }], 5 | "babel-plugin-relay" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.circleci/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM circleci/node:10.1.0 2 | 3 | ENV WATCHMAN_VERSION 4.9.0 4 | ENV PATH=$PATH:/home/circleci/.config/yarn/global/node_modules/.bin 5 | 6 | RUN set -ex; \ 7 | sudo apt-get update; \ 8 | sudo apt-get install -y autoconf automake build-essential python-dev libtool libssl-dev; \ 9 | cd /tmp && curl -LO https://github.com/facebook/watchman/archive/v${WATCHMAN_VERSION}.tar.gz; \ 10 | tar xzf v${WATCHMAN_VERSION}.tar.gz && rm v${WATCHMAN_VERSION}.tar.gz; \ 11 | cd watchman-${WATCHMAN_VERSION}; \ 12 | ./autogen.sh; ./configure; make; sudo make install; \ 13 | cd /tmp && sudo rm -rf watchman-${WATCHMAN_VERSION}; \ 14 | yarn global add firebase-tools --cache-folder /tmp/.cache; \ 15 | rm -rf /tmp/.cache; 16 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # Check https://circleci.com/docs/2.0/ for more details 3 | 4 | version: 2 5 | 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/node:dubnium 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | keys: 14 | - yarn-v1-{{ checksum "yarn.lock" }} 15 | - yarn-v1- 16 | - run: 17 | name: Install NPM modules 18 | command: | 19 | yarn cache dir 20 | yarn install --frozen-lockfile 21 | yarn add firebase-tools 22 | yarn run firebase --version 23 | - save_cache: 24 | key: yarn-v1-{{ checksum "yarn.lock" }} 25 | paths: 26 | - ~/.cache/yarn/v1 27 | - run: 28 | name: Build 29 | command: | 30 | yarn relay --watchman=false 31 | yarn build 32 | - run: 33 | name: Test 34 | command: | 35 | yarn lint 36 | yarn test --forceExit 37 | # - run: 38 | # name: Deploy 39 | # command: | 40 | # if [ "${CIRCLE_BRANCH}" == "master" ]; then 41 | # yarn run firebase use dev 42 | # yarn run firebase deploy --token=$FIREBASE_TOKEN 43 | # fi 44 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Application settings 2 | 3 | APP_NAME=React Starter Kit 4 | APP_DESCRIPTION=Bootstrap new web application projects with React.js and GraphQL 5 | APP_ORIGIN=http://localhost:3000 6 | APP_ENV=local 7 | APP_VERSION=latest 8 | 9 | # Google Cloud & Firebase 10 | # https://console.cloud.google.com/apis/credentials 11 | # https://console.firebase.google.com/project/_/settings/general/ 12 | # https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk 13 | 14 | GCP_PROJECT=example-dev 15 | GCP_BROWSER_KEY=AIzaSyAsuqpqt29-TIwBAu01Nbt5QnC3FIKO4A4 16 | GCP_SERVER_KEY=AIzaSyAsuqpqt29-TIwBAu01Nbt5QnC3FIKO4A4 17 | GCP_SERVICE_KEY={"type":"service_account","project_id":"example-dev","private_key_id":"...","private_key":"-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDMBPeEoIK6sSqj\nIj30SWCUicZPTzTKJJwfbRHfUlC/E/8S9WNDTqO16vM4SD1jpNH35/ei0pZcrmoP\n9Zk9wViEz9/NgzbDbBHWQzgpJVEvcY+uM8HQ8YvRXQ8eW415x+rmoGrJtVDl37V8\nD7FIrRxEv1PUCzh8/gFm0c2lLr8sUn/r4SaFpuj0mXu5+ZFZFuxjixvjodWx5n3v\n/0/D/uGWPKBNqcuFvXTflIMk+wnrLq91GJypGVVtmXoIWZ4iNVfjfGVcpOtUiilr\nwC/hU5Cn7LYEBHYoOWPnc/TCftePiC/ZwJL56L3Mn9MT3mtGtT0R+sfZKa6oFdfy\nhXJhatwZAgMBAAECggEAOyyboBewIzccv0lAv/iCb0LQxpMaJCFfOQw5GVV2Px2t\nJ5IN8uk9uZeGaQYm7B5TmjxpSowa+ZHLCIr7IfrQ0mC6sJCE00SmncdMZD7DH+gn\nvOadKh3NKHH93xe93psaKj9QCeYxqyLqMCwbBxHSt6voxAFnJnXD8U8b/vOilldo\nXi7JA8nu+oX3ySJzG0n5Ug5zYCS8zrBapxGWDW78orM7zvZ298xraf8ooOwSzj3X\njsNtbvvB/sUNwnnGH9wwDwLNrw5WavkUG26ugaMmzHBzN3YNpjK+eCG8CkJRV1cL\nBdBwDaTAk9DIQyWaw/LAl+Ha2eE9AUt0lABGFeydEwKBgQD43GW7mATLTYHO0xxM\naoTFSpHM/IJNa49THhv6Nv+G1DIBLq1LlrSDncQOsoP0f/jwnLKcwAAg9DoYnnd5\nygoE9Nh8Lgejk2xW9NTyH4M7KlV3sOpV9OOAzE7hXYCVBoyI379Wgs/pqLeGpIwJ\nMIx/Gl4Z8G0AcAupxZKhQuxlOwKBgQDR30JTW93jcHfnhnCATr/F38XnL+agGZy6\n2BR2axeU+Ct2W8hLFUVoylkycqF7yD/QtW/EjZOPF0RKOM6IHiV3dCyJoz534+x4\nA/0e0UyrEdG5g7W+O79bWow+ER5YYl22TgDOmE+UFlBO6fJW9+I0YvTqki+YSwlT\nrwA+MqAeuwKBgQCPXJgWm5qXa80N0rwIoYxfA3g+uHBwHThxz3Sajjhh+bfcyoD2\nfJj9AVPCi8BMh7RnGD4k4s6wLUGSkSeOt39SH6Le1r171B+jcGOEH/c/jEG0M+yr\nG+o7dncyiOTb9OvcpdjaA322w4UGQaCSYq9tQUlYdBK3H9T4NmMkFyOLpQKBgEwD\nmilJF9f971/rQKooW6tWvn5ayiRowmymQNsXNMZfEJbg7W3MeYRX7fCotjZ4NCzq\n2l2NjcmA+toLMzr3+EgIyuzbNJAF/KsHftF/q042uQiBXP1W9Jso86yzVJNcpWaX\nYBFz9zbC0jmS4JSBWevxf5XKdvSpEOq/cs4UVgxrAoGBAJOKGKB8LgsbH9DgTMQs\nByKYONEe64YfKEXDJ1sdYttz3C9EZyGRICYpnEbpBUmMa82aYT7MWinYa2jHY0Py\nyFzi4aS5gRgqF6KFRWfbkkXDupwgS8/KJo56MX2Uie1E7fGc7TCtVRW1n4HE5oxt\ndQpBhaUyp0K8c5U4A+BGdwkK\n-----END PRIVATE KEY-----\n","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":"..."} 18 | FIREBASE_CONFIG={"projectId":"example-dev"} 19 | 20 | # Authentication 21 | 22 | JWT_NAME=__session_rsk 23 | JWT_SECRET=xxxxx 24 | 25 | GOOGLE_CLIENT_ID=xxxxx 26 | GOOGLE_CLIENT_SECRET=xxxxx 27 | 28 | FACEBOOK_APP_ID=xxxxx 29 | FACEBOOK_APP_SECRET=xxxxx 30 | FACEBOOK_PAGE_ID=xxxxx 31 | 32 | # PostgreSQL 33 | # https://www.postgresql.org/docs/current/static/libpq-envars.html 34 | 35 | PGHOST=localhost 36 | PGUSER=postgres 37 | PGDATABASE=app 38 | PGPASSWORD= 39 | PGAPPNAME=rsk_dev 40 | # PGSSLMODE=require 41 | # PGSSLCERT=./ssl/client-cert.pem 42 | # PGSSLKEY=./ssl/client-key.pem 43 | # PGSSLROOTCERT=./ssl/server-ca.pem 44 | # PGDEBUG=true 45 | 46 | # Analytics 47 | 48 | GA_TRACKING_ID= 49 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # Environment variables for the production build (NODE_ENV=production) 2 | # 3 | # IMPORTANT NODE: 4 | # Do not include any API keys, secrets, passwords into this file in favor of 5 | # using Firebase Config API or something similar. For example: 6 | # 7 | # $ firebase --project=example-prod functions:config:set \ 8 | # app.app_origin="https://example.com" \ 9 | # app.gcp_service_key="xxxxx" \ 10 | # app.gcp_server_key="xxxxx" \ 11 | # app.jwt_secret="xxxxx" \ 12 | # app.google_client_id="xxxxx" \ 13 | # app.google_client_secret="xxxxx" \ 14 | # app.facebook_app_id="xxxxx" \ 15 | # app.facebook_app_secret="xxxxx" \ 16 | # app.pgdatabase="xxxxx" \ 17 | # app.pgpassword="xxxxx" 18 | # 19 | 20 | # Authentication 21 | 22 | JWT_NAME=__session 23 | # JWT_SECRET=xxxxx 24 | 25 | # GOOGLE_CLIENT_ID=xxxxx 26 | # GOOGLE_CLIENT_SECRET=xxxxx 27 | 28 | # FACEBOOK_APP_ID=xxxxx 29 | # FACEBOOK_APP_SECRET=xxxxx 30 | 31 | # PostgreSQL 32 | # https://www.postgresql.org/docs/current/static/libpq-envars.html 33 | 34 | PGHOST=/cloudsql/:: 35 | # PGUSER= 36 | # PGDATABASE= 37 | # PGPASSWORD= 38 | PGAPPNAME=rsk 39 | PGSSLMODE= 40 | PGDEBUG=false 41 | 42 | # Analytics 43 | 44 | # GA_TRACKING_ID=UA-XXXXX-Y 45 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # Environment variables for the test build (NODE_ENV=test) 2 | 3 | # APP_ORIGIN=http://localhost:3000 4 | 5 | # Google Cloud & Firebase 6 | # https://console.cloud.google.com/apis/credentials 7 | # https://console.firebase.google.com/project/_/settings/general/ 8 | # https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk 9 | 10 | # GCP_PROJECT=example-test 11 | # GCP_BROWSER_KEY=AIzaSyAsuqpqt29-TIwBAu01Nbt5QnC3FIKO4A4 12 | # GCP_SERVER_KEY=AIzaSyAsuqpqt29-TIwBAu01Nbt5QnC3FIKO4A4 13 | # GCP_SERVICE_KEY={"type":"service_account","project_id":"example-test","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":"..."} 14 | 15 | # Authentication 16 | 17 | JWT_NAME=__session 18 | # JWT_SECRET=xxxxx 19 | 20 | # GOOGLE_CLIENT_ID=xxxxx 21 | # GOOGLE_CLIENT_SECRET=xxxxx 22 | 23 | # FACEBOOK_APP_ID=xxxxx 24 | # FACEBOOK_APP_SECRET=xxxxx 25 | 26 | # PostgreSQL 27 | # https://www.postgresql.org/docs/current/static/libpq-envars.html 28 | 29 | PGHOST=/cloudsql/:: 30 | PGUSER= 31 | PGDATABASE= 32 | PGPASSWORD= 33 | PGAPPNAME=rsk_test 34 | 35 | # Analytics 36 | 37 | # GA_TRACKING_ID= 38 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-react-app", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | 2 | # Automatically normalize line endings for all text-based files 3 | # https://git-scm.com/docs/gitattributes#_end_of_line_conversion 4 | 5 | * text=auto 6 | 7 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 8 | 9 | # For the following file types, normalize line endings to LF on 10 | # checkin and prevent conversion to CRLF when they are checked out 11 | # (this is required in order to prevent newline related issues like, 12 | # for example, after the build script is run) 13 | 14 | .* text eol=lf 15 | *.css text eol=lf 16 | *.html text eol=lf 17 | *.js text eol=lf 18 | *.json text eol=lf 19 | *.md text eol=lf 20 | *.txt text eol=lf 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: koistya 4 | open_collective: react-firebase-starter 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # See https://help.github.com/ignore-files/ for more about ignoring files 3 | 4 | # Build output 5 | build/ 6 | 7 | # Dependencies 8 | node_modules/ 9 | 10 | # Testing 11 | coverage/ 12 | 13 | # Logs 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | firebase-debug.log* 18 | firebase-error.log* 19 | 20 | # VS Code 21 | .vscode/* 22 | !.vscode/snippets 23 | !.vscode/launch.json 24 | !.vscode/settings.json 25 | 26 | # Misc 27 | .firebase/* 28 | __generated__ 29 | .DS_Store 30 | .env.local 31 | .env.*.local 32 | .eslintcache 33 | .yarn-integrity 34 | backup.sql 35 | VERSION 36 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | __generated__ 4 | package.json 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Start", 11 | "program": "${workspaceFolder}/node_modules/react-app-tools/scripts/start.js" 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "name": "Build", 17 | "program": "${workspaceFolder}/node_modules/react-app-tools/scripts/build.js" 18 | }, 19 | { 20 | "type": "node", 21 | "request": "launch", 22 | "name": "Test", 23 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-app", 24 | "args": ["test", "--runInBand", "--no-cache", "--watchAll=false"], 25 | "cwd": "${workspaceRoot}", 26 | "protocol": "inspector", 27 | "console": "integratedTerminal", 28 | "internalConsoleOptions": "neverOpen" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "javascript.format.enable": false, 4 | "javascript.validate.enable": false, 5 | "vsicons.presets.angular": false, 6 | "[html]": { 7 | "editor.formatOnSave": false 8 | }, 9 | "search.exclude": { 10 | "**/build/**": true, 11 | "**/node_modules/**": true, 12 | "**/__generated__/**": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/snippets/javascript.json: -------------------------------------------------------------------------------- 1 | { 2 | "Route": { 3 | "prefix": "route", 4 | "body": [ 5 | "import React from 'react';", 6 | "import { graphql } from 'relay-runtime';", 7 | "import Layout from '../common/Layout';", 8 | "", 9 | "export default [", 10 | " {", 11 | " path: '/${1:path}',", 12 | " query: graphql`", 13 | " query ${TM_DIRECTORY/.*[\\/](.*)$/$1/}${2:Page}Query {", 14 | " ...Layout_data", 15 | " ...${2:Page}_data", 16 | " }", 17 | " `,", 18 | " components: () => [import(/* webpackChunkName: '${1:path}' */ './${2:Page}')],", 19 | " render: ([${2:Page}], data, { config }) => ({", 20 | " title: `${3:Title} • ${config.app.name}`,", 21 | " component: (", 22 | " ", 23 | " <${2:Page} data={data} />", 24 | " ", 25 | " ),", 26 | " chunks: ['${1:path}'],", 27 | " }),", 28 | " },", 29 | "];", 30 | "" 31 | ], 32 | "description": "Route" 33 | }, 34 | "ReactComponent": { 35 | "prefix": "reactComponent", 36 | "body": [ 37 | "import clsx from 'clsx';", 38 | "import React from 'react';", 39 | "import { makeStyles } from '@material-ui/core/styles';", 40 | "", 41 | "const useStyles = makeStyles(theme => ({", 42 | " root: {},", 43 | "}));", 44 | "", 45 | "function ${1:Component}(props) {", 46 | " const { className, ...other } = props;", 47 | " const s = useStyles();", 48 | "", 49 | " return (", 50 | " <${2:div} className={clsx(s.root, className)} {...other}>", 51 | " ${3:body}$0", 52 | " ", 53 | " );", 54 | "}", 55 | "", 56 | "export default ${1:Component};", 57 | "" 58 | ], 59 | "description": "React Component" 60 | }, 61 | "ReactRefComponent": { 62 | "prefix": "reactRefComponent", 63 | "body": [ 64 | "import clsx from 'clsx';", 65 | "import React from 'react';", 66 | "import { makeStyles } from '@material-ui/core/styles';", 67 | "", 68 | "const useStyles = makeStyles(theme => ({", 69 | " root: {},", 70 | "}));", 71 | "", 72 | "const ${1:Component} = React.forwardRef(function ${1:Component}(props, ref) {", 73 | " const { className, ...other } = props;", 74 | " const s = useStyles();", 75 | "", 76 | " return (", 77 | " <${2:div} className={clsx(s.root, className)} ref={ref} {...other}>", 78 | " ${3:body}$0", 79 | " ", 80 | " );", 81 | "});", 82 | "", 83 | "export default ${1:Component};", 84 | "" 85 | ], 86 | "description": "React Ref Component" 87 | }, 88 | "React/Relay Fragment Container": { 89 | "prefix": "reactFragmentContainer", 90 | "body": [ 91 | "import clsx from 'clsx';", 92 | "import React from 'react';", 93 | "import { makeStyles } from '@material-ui/core/styles';", 94 | "import { createFragmentContainer, graphql } from 'react-relay';", 95 | "", 96 | "const useStyles = makeStyles(theme => ({", 97 | " root: {},", 98 | "}));", 99 | "", 100 | "function ${1:Component}(props) {", 101 | " const { className, data, ...other } = props;", 102 | " const s = useStyles();", 103 | "", 104 | " return (", 105 | " <${2:div} className={clsx(s.root, className)} {...other}>", 106 | " ${3:body}$0", 107 | " ", 108 | " );", 109 | "}", 110 | "", 111 | "export default createFragmentContainer(${1:Component}, {", 112 | " data: graphql`", 113 | " fragment ${1:Component}_data on Query {", 114 | " id", 115 | " }", 116 | " `,", 117 | "});", 118 | "" 119 | ], 120 | "description": "React/Relay Fragment Container" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015-present Kriasoft. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "source": ".", 4 | "ignore": [ 5 | ".circleci", 6 | ".DS_Store", 7 | ".firebase", 8 | ".git", 9 | ".vscode", 10 | "build/public/**", 11 | "coverage", 12 | "migrations", 13 | "node_modules", 14 | "public", 15 | "scripts", 16 | "seeds", 17 | "src", 18 | ".babelrc", 19 | ".editorconfig", 20 | ".env.local", 21 | ".env.*.local", 22 | ".eslintrc", 23 | ".gitattributes", 24 | ".gitignore", 25 | ".prettierignore", 26 | ".prettierrc", 27 | "knexfile.js", 28 | "LICENSE.txt", 29 | "README.md", 30 | "schema.graphql", 31 | "storage.rules", 32 | "yarn-debug.log*", 33 | "yarn-error.log*" 34 | ] 35 | }, 36 | "hosting": { 37 | "public": "build/public", 38 | "rewrites": [ 39 | { 40 | "source": "**", 41 | "function": "app" 42 | } 43 | ], 44 | "headers": [ 45 | { 46 | "source": "service-worker.js", 47 | "headers": [ 48 | { 49 | "key": "Cache-Control", 50 | "value": "no-cache, no-store, must-revalidate" 51 | } 52 | ] 53 | } 54 | ] 55 | }, 56 | "storage": { 57 | "rules": "storage.rules" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | const fs = require('fs'); 8 | const cp = require('child_process'); 9 | const dotenv = require('dotenv'); 10 | const { env = 'dev' } = require('minimist')(process.argv.slice(2)); 11 | 12 | function getProjectID(env) { 13 | return `example-${env}`; 14 | } 15 | 16 | // Load API keys, secrets etc. from Firebase environment 17 | // https://firebase.google.com/docs/functions/config-env 18 | if (env === 'prod' || env === 'test') { 19 | const { status, stdout } = cp.spawnSync( 20 | 'firebase', 21 | [`--project=${getProjectID(env)}`, 'functions:config:get'], 22 | { stdio: ['pipe', 'pipe', 'inherit'] }, 23 | ); 24 | 25 | if (status !== 0) process.exit(status); 26 | 27 | const config = JSON.parse(stdout.toString()).app; 28 | 29 | Object.keys(config).forEach(key => { 30 | process.env[key.toUpperCase()] = 31 | typeof key === 'object' ? JSON.stringify(config[key]) : config[key]; 32 | }); 33 | 34 | process.env.PGHOST = 'X.X.X.X'; 35 | process.env.PGPOST = '5432'; 36 | process.env.PGSSLMODE = 'require'; 37 | process.env.PGSSLCERT = `./ssl/${env}.client-cert.pem`; 38 | process.env.PGSSLKEY = `./ssl/${env}.client-key.pem`; 39 | process.env.PGSSLROOTCERT = `./ssl/${env}.server-ca.pem`; 40 | } else if (env === 'local') { 41 | dotenv.config({ path: '.env.local' }); 42 | process.env.PGPORT = process.env.PGPORT || '5432'; 43 | process.env.PGHOST = process.env.PGHOST || 'localhost'; 44 | process.env.PGUSER = process.env.PGUSER || 'postgres'; 45 | process.env.PGPASSWORD = process.env.PGPASSWORD || ''; 46 | process.env.PGDATABASE = process.env.PGDATABASE || 'rsk_local'; 47 | process.env.PGSSLMODE = process.env.PGSSLMODE || 'disable'; 48 | } 49 | 50 | console.log('Environment:', env); 51 | dotenv.config({ path: '.env' }); 52 | 53 | // Knex configuration that is used with DB migration scripts etc. 54 | // http://knexjs.org/#knexfile 55 | module.exports = { 56 | client: 'pg', 57 | migrations: { 58 | tableName: 'migrations', 59 | }, 60 | connection: { 61 | max: 1, 62 | ssl: (process.env.PGSSLMODE || 'disable') !== 'disable' && { 63 | rejectUnauthorized: false, 64 | cert: fs.readFileSync(process.env.PGSSLCERT, 'utf8'), 65 | key: fs.readFileSync(process.env.PGSSLKEY, 'utf8'), 66 | ca: fs.readFileSync(process.env.PGSSLROOTCERT, 'utf8'), 67 | }, 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /migrations/20180101000000_initial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | /* prettier-ignore */ 8 | 9 | exports.up = async db => { 10 | await db.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); 11 | await db.raw('CREATE EXTENSION IF NOT EXISTS "hstore"'); 12 | 13 | await db.schema.createTable('users', table => { 14 | table.uuid('id').notNullable().defaultTo(db.raw('uuid_generate_v4()')).primary(); 15 | table.string('username', 50).unique(); 16 | table.string('email', 100); 17 | table.boolean('email_verified').notNullable().defaultTo(false); 18 | table.string('display_name', 100); 19 | table.string('photo_url', 250); 20 | table.string('time_zone', 50); 21 | table.boolean('is_admin').notNullable().defaultTo(false); 22 | table.timestamps(false, true); 23 | table.timestamp('last_login_at').notNullable().defaultTo(db.fn.now()); 24 | }); 25 | 26 | await db.schema.createTable('user_tokens', table => { 27 | table.uuid('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); 28 | table.uuid('token_id').notNullable().primary(); 29 | table.timestamp('created_at').notNullable().defaultTo(db.fn.now()); 30 | }); 31 | 32 | await db.schema.createTable('user_identities', table => { 33 | table.uuid('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); 34 | table.string('provider', 16).notNullable(); 35 | table.string('provider_id', 36).notNullable(); 36 | table.jsonb('profile').notNullable(); 37 | table.jsonb('credentials').notNullable(); 38 | table.timestamps(false, true); 39 | table.primary(['provider', 'provider_id']); 40 | }); 41 | 42 | await db.schema.createTable('stories', table => { 43 | table.uuid('id').notNullable().defaultTo(db.raw('uuid_generate_v4()')).primary(); 44 | table.uuid('author_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); 45 | table.string('slug', 120).notNullable(); 46 | table.string('title', 120).notNullable(); 47 | table.string('text', 2000); 48 | table.boolean('is_url').notNullable().defaultTo(false); 49 | table.boolean('approved').notNullable().defaultTo(false); 50 | table.timestamps(false, true); 51 | }); 52 | 53 | await db.schema.createTable('story_points', table => { 54 | table.uuid('story_id').references('id').inTable('stories').onDelete('CASCADE').onUpdate('CASCADE'); 55 | table.uuid('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); 56 | table.primary(['story_id', 'user_id']); 57 | }); 58 | 59 | await db.schema.createTable('comments', table => { 60 | table.uuid('id').notNullable().defaultTo(db.raw('uuid_generate_v4()')).primary(); 61 | table.uuid('story_id').notNullable().references('id').inTable('stories').onDelete('CASCADE').onUpdate('CASCADE'); 62 | table.uuid('parent_id').references('id').inTable('comments').onDelete('CASCADE').onUpdate('CASCADE'); 63 | table.uuid('author_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); 64 | table.text('text'); 65 | table.timestamps(false, true); 66 | }); 67 | 68 | await db.schema.createTable('comment_points', table => { 69 | table.uuid('comment_id').references('id').inTable('comments').onDelete('CASCADE').onUpdate('CASCADE'); 70 | table.uuid('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE').onUpdate('CASCADE'); 71 | table.primary(['comment_id', 'user_id']); 72 | }); 73 | }; 74 | 75 | exports.down = async db => { 76 | await db.schema.dropTableIfExists('comment_points'); 77 | await db.schema.dropTableIfExists('comments'); 78 | await db.schema.dropTableIfExists('story_points'); 79 | await db.schema.dropTableIfExists('stories'); 80 | await db.schema.dropTableIfExists('user_identities'); 81 | await db.schema.dropTableIfExists('user_tokens'); 82 | await db.schema.dropTableIfExists('users'); 83 | }; 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "build/server.js", 6 | "engines": { 7 | "node": "10" 8 | }, 9 | "browserslist": [ 10 | ">0.2%", 11 | "not dead", 12 | "not ie <= 11", 13 | "not op_mini all" 14 | ], 15 | "dependencies": { 16 | "@babel/polyfill": "^7.7.0", 17 | "@babel/runtime": "^7.7.7", 18 | "@firebase/app": "^0.5.1", 19 | "@firebase/auth": "^0.13.4", 20 | "@material-ui/core": "^4.8.3", 21 | "@material-ui/icons": "^4.5.1", 22 | "body-parser": "^1.19.0", 23 | "clsx": "^1.0.4", 24 | "cookie": "^0.4.0", 25 | "cookie-parser": "^1.4.4", 26 | "dataloader": "^2.0.0", 27 | "dotenv": "^8.2.0", 28 | "ejs": "^3.0.1", 29 | "express": "^4.17.1", 30 | "express-graphql": "^0.9.0", 31 | "firebase-admin": "^8.9.0", 32 | "firebase-functions": "^3.3.0", 33 | "got": "^10.2.2", 34 | "graphql": "^14.5.8", 35 | "graphql-relay": "^0.6.0", 36 | "history": "^4.10.1", 37 | "hoist-non-react-statics": "^3.3.1", 38 | "idx": "^2.5.6", 39 | "jsonwebtoken": "^8.5.1", 40 | "jwt-passport": "^0.0.5", 41 | "knex": "^0.20.7", 42 | "load-script": "^1.0.0", 43 | "lodash": "^4.17.15", 44 | "moment-timezone": "^0.5.27", 45 | "passport": "^0.4.1", 46 | "passport-facebook": "^3.0.0", 47 | "passport-google-oauth20": "^2.0.0", 48 | "pg": "^7.17.0", 49 | "prop-types": "^15.7.2", 50 | "query-string": "^6.9.0", 51 | "react": "^16.12.0", 52 | "react-dom": "^16.12.0", 53 | "react-relay": "^8.0.0", 54 | "recompose": "^0.30.0", 55 | "relay-runtime": "^8.0.0", 56 | "request": "^2.88.0", 57 | "request-promise-native": "^1.0.8", 58 | "serialize-javascript": "^2.1.2", 59 | "slugify": "^1.3.6", 60 | "universal-router": "^8.3.0", 61 | "uuid": "^3.3.3", 62 | "validator": "^12.1.0" 63 | }, 64 | "devDependencies": { 65 | "@babel/core": "^7.7.7", 66 | "@babel/register": "^7.7.7", 67 | "babel-plugin-lodash": "^3.3.4", 68 | "babel-plugin-relay": "^8.0.0", 69 | "chai": "^4.2.0", 70 | "eslint-config-prettier": "^6.9.0", 71 | "eslint-plugin-prettier": "^3.1.2", 72 | "husky": "^4.0.4", 73 | "lint-staged": "^9.5.0", 74 | "minimist": "^1.2.0", 75 | "prettier": "^1.19.1", 76 | "raw-loader": "^4.0.0", 77 | "react-app-tools": "^3.1.0-preview.7", 78 | "relay-compiler": "^8.0.0" 79 | }, 80 | "lint-staged": { 81 | "*.js": [ 82 | "yarn run eslint --no-ignore --fix --max-warnings=0", 83 | "yarn run prettier --write", 84 | "git add --force" 85 | ], 86 | "*.json": [ 87 | "prettier --write", 88 | "git add --force" 89 | ] 90 | }, 91 | "husky": { 92 | "hooks": { 93 | "pre-commit": "lint-staged" 94 | } 95 | }, 96 | "scripts": { 97 | "setup": "node ./scripts/setup", 98 | "update-schema": "node ./scripts/update-schema", 99 | "relay": "relay-compiler --src ./src --schema ./schema.graphql", 100 | "prestart": "yarn relay", 101 | "start": "react-app start", 102 | "build": "react-app build", 103 | "test": "react-app test", 104 | "lint": "eslint --ignore-path .gitignore --ignore-pattern \"!**/.*\" .", 105 | "lint-fix": "eslint --ignore-path .gitignore --ignore-pattern \"!**/.*\" --fix . && yarn run prettier --write \"**/*.{js,json}\"", 106 | "db-backup": "node ./scripts/db-backup", 107 | "db-restore": "node ./scripts/db-restore", 108 | "db-change": "knex migrate:make", 109 | "db-migrate": "knex migrate:latest", 110 | "db-rollback": "knex migrate:rollback", 111 | "db-seed": "knex seed:run", 112 | "db-version": "knex migrate:currentVersion", 113 | "db-reset-dev": "yarn db-rollback --env=dev && yarn db-migrate --env=dev && yarn db-restore --env=dev", 114 | "db-reset-test": "yarn db-rollback --env=test && yarn db-migrate --env=test && yarn db-restore --env=test", 115 | "db-reset-prod": "yarn db-rollback --env=prod && yarn db-migrate --env=prod && yarn db-restore --env=prod", 116 | "db": "node --experimental-repl-await ./scripts/db", 117 | "psql": "node ./scripts/psql", 118 | "deploy": "yarn run deploy-test", 119 | "deploy-test": "node ./scripts/pre-deploy --env=test && firebase --project=example-test deploy && node ./scripts/post-deploy --env=test", 120 | "deploy-prod": "node ./scripts/pre-deploy --env=prod && firebase --project=example-prod deploy && node ./scripts/post-deploy --env=prod" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriasoft/react-firebase-starter/69328e57cbc832c87b9cce9a9955fa21515545de/public/favicon.ico -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "React Starter Kit for Firebase", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | enum AuthenticationProvider { 2 | GOOGLE 3 | TWITTER 4 | FACEBOOK 5 | } 6 | 7 | type Comment implements Node { 8 | # The ID of an object 9 | id: ID! 10 | story: Story! 11 | parent: Comment 12 | author: User! 13 | comments: [Comment] 14 | text: String 15 | pointsCount: Int! 16 | createdAt(format: String): String 17 | updatedAt(format: String): String 18 | } 19 | 20 | input DeleteUserInput { 21 | id: ID! 22 | clientMutationId: String 23 | } 24 | 25 | type DeleteUserPayload { 26 | deletedUserId: String 27 | clientMutationId: String 28 | } 29 | 30 | type Identity { 31 | # The ID of an object 32 | id: ID! 33 | provider: AuthenticationProvider 34 | providerId: String 35 | email: String 36 | displayName: String 37 | photoURL: String 38 | profileURL: String 39 | } 40 | 41 | input LikeStoryInput { 42 | id: ID! 43 | clientMutationId: String 44 | } 45 | 46 | type LikeStoryPayload { 47 | story: Story 48 | clientMutationId: String 49 | } 50 | 51 | type Mutation { 52 | # Updates a user. 53 | updateUser(input: UpdateUserInput!): UpdateUserPayload 54 | 55 | # Deletes a user. 56 | deleteUser(input: DeleteUserInput!): DeleteUserPayload 57 | 58 | # Creates or updates a story. 59 | upsertStory(input: UpsertStoryInput!): UpsertStoryPayload 60 | 61 | # Marks the story as "liked". 62 | likeStory(input: LikeStoryInput!): LikeStoryPayload 63 | } 64 | 65 | # An object with an ID 66 | interface Node { 67 | # The id of the object. 68 | id: ID! 69 | } 70 | 71 | # Information about pagination in a connection. 72 | type PageInfo { 73 | # When paginating forwards, are there more items? 74 | hasNextPage: Boolean! 75 | 76 | # When paginating backwards, are there more items? 77 | hasPreviousPage: Boolean! 78 | 79 | # When paginating backwards, the cursor to continue. 80 | startCursor: String 81 | 82 | # When paginating forwards, the cursor to continue. 83 | endCursor: String 84 | } 85 | 86 | type Query { 87 | # Fetches an object given its ID 88 | node( 89 | # The ID of an object 90 | id: ID! 91 | ): Node 92 | 93 | # Fetches objects given their IDs 94 | nodes( 95 | # The IDs of objects 96 | ids: [ID!]! 97 | ): [Node]! 98 | me: User 99 | user(username: String!): User 100 | users(after: String, first: Int): UserConnection 101 | story(slug: String!): Story 102 | stories: [Story] 103 | } 104 | 105 | type Story implements Node { 106 | # The ID of an object 107 | id: ID! 108 | author: User! 109 | slug: String! 110 | title: String! 111 | text(truncate: Int): String! 112 | isURL: Boolean! 113 | comments: [Comment] 114 | pointsCount: Int! 115 | pointGiven: Boolean! 116 | commentsCount: Int! 117 | createdAt(format: String): String 118 | updatedAt(format: String): String 119 | } 120 | 121 | input UpdateUserInput { 122 | id: ID! 123 | username: String 124 | email: String 125 | displayName: String 126 | photoURL: String 127 | timeZone: String 128 | isAdmin: Boolean 129 | validateOnly: Boolean 130 | clientMutationId: String 131 | } 132 | 133 | type UpdateUserPayload { 134 | user: User 135 | clientMutationId: String 136 | } 137 | 138 | input UpsertStoryInput { 139 | id: ID 140 | title: String 141 | text: String 142 | approved: Boolean 143 | validateOnly: Boolean 144 | clientMutationId: String 145 | } 146 | 147 | type UpsertStoryPayload { 148 | story: Story 149 | clientMutationId: String 150 | } 151 | 152 | type User implements Node { 153 | # The ID of an object 154 | id: ID! 155 | username: String! 156 | email: String 157 | displayName: String 158 | photoURL: String 159 | timeZone: String 160 | identities: [Identity] 161 | isAdmin: Boolean 162 | firebaseToken: String 163 | createdAt(format: String): String 164 | updatedAt(format: String): String 165 | lastLoginAt(format: String): String 166 | } 167 | 168 | # A connection to a list of items. 169 | type UserConnection { 170 | # Information to aid in pagination. 171 | pageInfo: PageInfo! 172 | 173 | # A list of edges. 174 | edges: [UserEdge] 175 | totalCount: Int! 176 | } 177 | 178 | # An edge in a connection. 179 | type UserEdge { 180 | # The item at the end of the edge 181 | node: User 182 | 183 | # A cursor for use in pagination 184 | cursor: String! 185 | } 186 | -------------------------------------------------------------------------------- /scripts/db-backup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | const fs = require('fs'); 8 | const readline = require('readline'); 9 | const cp = require('child_process'); 10 | const { EOL } = require('os'); 11 | 12 | // Load environment variables (PGHOST, PGUSER, etc.) 13 | require('../knexfile'); 14 | 15 | // Ensure that the SSL key file has correct permissions 16 | if (process.env.PGSSLKEY) { 17 | cp.spawnSync('chmod', ['0600', process.env.PGSSLKEY], { stdio: 'inherit' }); 18 | } 19 | 20 | // Get the list of database tables 21 | let cmd = cp.spawnSync( 22 | 'psql', 23 | [ 24 | '--no-align', 25 | '--tuples-only', 26 | '--record-separator=|', 27 | '--command', 28 | "SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'", 29 | ], 30 | { 31 | stdio: ['inherit', 'pipe', 'inherit'], 32 | }, 33 | ); 34 | 35 | if (cmd.status !== 0) { 36 | console.error('Failed to read the list of database tables.'); 37 | process.exit(cmd.status); 38 | } 39 | 40 | const tables = cmd.stdout 41 | .toString('utf8') 42 | .trim() 43 | .split('|') 44 | .filter(x => x !== 'migrations' && x !== 'migrations_lock') 45 | .map(x => `public."${x}"`) 46 | .join(', '); 47 | 48 | // Dump the database 49 | cmd = cp 50 | .spawn( 51 | 'pg_dump', 52 | [ 53 | '--verbose', 54 | '--data-only', 55 | '--no-owner', 56 | '--no-privileges', 57 | // '--column-inserts', 58 | '--disable-triggers', 59 | '--exclude-table=migrations', 60 | '--exclude-table=migrations_lock', 61 | '--exclude-table=migrations_id_seq', 62 | '--exclude-table=migrations_lock_index_seq', 63 | ...process.argv.slice(2).filter(x => !x.startsWith('--env')), 64 | ], 65 | { 66 | stdio: ['pipe', 'pipe', 'inherit'], 67 | }, 68 | ) 69 | .on('exit', code => { 70 | if (code !== 0) process.exit(code); 71 | }); 72 | 73 | const out = fs.createWriteStream('backup.sql', { encoding: 'utf8' }); 74 | const rl = readline.createInterface({ input: cmd.stdout, terminal: false }); 75 | 76 | rl.on('line', line => { 77 | // Some (system) triggers cannot be disabled in a cloud environment 78 | // "DISABLE TRIGGER ALL" => "DISABLE TRIGGER USER" 79 | if (line.endsWith(' TRIGGER ALL;')) { 80 | out.write(`${line.substr(0, line.length - 5)} USER;${EOL}`, 'utf8'); 81 | } 82 | // Add a command that truncates all the database tables 83 | else if (line.startsWith('SET row_security')) { 84 | out.write(`${line}${EOL}${EOL}TRUNCATE TABLE ${tables} CASCADE;${EOL}`); 85 | } else { 86 | out.write(`${line}${EOL}`, 'utf8'); 87 | } 88 | }); 89 | -------------------------------------------------------------------------------- /scripts/db-restore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | const cp = require('child_process'); 8 | 9 | // Load environment variables (PGHOST, PGUSER, etc.) 10 | require('../knexfile'); 11 | 12 | // Ensure that the SSL key file has correct permissions 13 | if (process.env.PGSSLKEY) { 14 | cp.spawnSync('chmod', ['0600', process.env.PGSSLKEY], { stdio: 'inherit' }); 15 | } 16 | 17 | cp.spawn( 18 | 'psql', 19 | [ 20 | '--file=backup.sql', 21 | '--echo-errors', 22 | '--no-readline', 23 | ...process.argv.slice(2).filter(x => !x.startsWith('--env')), 24 | ], 25 | { 26 | stdio: 'inherit', 27 | }, 28 | ).on('exit', process.exit); 29 | -------------------------------------------------------------------------------- /scripts/db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | const repl = require('repl'); 8 | const knex = require('knex'); 9 | const config = require('../knexfile'); 10 | 11 | global.db = knex(config); 12 | 13 | global.db 14 | .raw('select current_database(), version()') 15 | .then(({ rows: [x] }) => { 16 | console.log('Connected to', x.current_database); 17 | console.log(x.version); 18 | repl.start('#> ').on('exit', process.exit); 19 | }) 20 | .catch(err => { 21 | console.error(err); 22 | process.exit(1); 23 | }); 24 | -------------------------------------------------------------------------------- /scripts/post-deploy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | const got = require('got'); 8 | 9 | // Load environment variables 10 | require('../knexfile'); 11 | 12 | // The list of URLs to purge from CDN cache after deployment is complete 13 | const urls = [ 14 | '/', 15 | '/about', 16 | '/news', 17 | '/submit', 18 | '/account', 19 | '/privacy', 20 | '/terms', 21 | ]; 22 | 23 | const options = { 24 | baseUrl: process.env.APP_ORIGIN, 25 | method: 'PURGE', 26 | }; 27 | 28 | Promise.all(urls.map(path => got(path, options))).catch(err => { 29 | console.error(err.stack); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /scripts/pre-deploy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const cp = require('child_process'); 10 | const { env } = require('minimist')(process.argv.slice(2)); 11 | 12 | // Writes the current version number to ./VERSION file 13 | const { status, stdout } = cp.spawnSync( 14 | 'git', 15 | ['show', '--no-patch', '--format=%cd+%h', '--date=format:%Y.%m.%d'], 16 | { stdio: ['pipe', 'pipe', 'inherit'] }, 17 | ); 18 | 19 | if (status === 0) { 20 | fs.writeFileSync('./VERSION', stdout.toString().trim(), 'utf8'); 21 | } else { 22 | process.exit(status); 23 | } 24 | 25 | // Generates ./build/public/robots.txt file. See https://robotstxt.org/ 26 | fs.writeFileSync( 27 | path.resolve(__dirname, '../build/public/robots.txt'), 28 | env === 'prod' ? 'User-agent: *\nDisallow:' : 'User-agent: *\nDisallow: /', 29 | 'utf8', 30 | ); 31 | -------------------------------------------------------------------------------- /scripts/psql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | const cp = require('child_process'); 8 | 9 | // Load environment variables (PGHOST, PGUSER, etc.) 10 | require('../knexfile'); 11 | 12 | // Ensure that the SSL key file has correct permissions 13 | if (process.env.PGSSLKEY) { 14 | cp.spawnSync('chmod', ['0600', process.env.PGSSLKEY], { stdio: 'inherit' }); 15 | } 16 | 17 | // Launch interactive terminal for working with Postgres 18 | cp.spawn('psql', { stdio: 'inherit' }); 19 | -------------------------------------------------------------------------------- /scripts/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | const cp = require('child_process'); 8 | 9 | let db; 10 | let status; 11 | 12 | (async () => { 13 | // Install Node.js dependencies 14 | ({ status } = cp.spawnSync('yarn', ['install'], { stdio: 'inherit' })); 15 | if (status !== 0) process.exit(status); 16 | 17 | const knex = require('knex'); 18 | const config = require('../knexfile'); 19 | 20 | // Check if database already exists 21 | const database = process.env.PGDATABASE; 22 | 23 | db = knex({ 24 | ...config, 25 | connection: { ...config.connection, database: 'postgres' }, 26 | }); 27 | 28 | const { 29 | rowCount, 30 | } = await db.raw('SELECT 1 FROM pg_database WHERE datname = ?', [database]); 31 | 32 | // Create a new database if it doesn't exist 33 | if (!rowCount) { 34 | console.log(`Creating a new database "${database}"...`); 35 | await db.raw('CREATE DATABASE ??', [database]); 36 | } 37 | 38 | await db.destroy(); 39 | 40 | db = knex(config); 41 | 42 | await db.destroy(); 43 | 44 | // Migrate database schema to the latest version 45 | ({ status } = cp.spawnSync('yarn', ['db-migrate'], { stdio: 'inherit' })); 46 | if (status !== 0) process.exit(status); 47 | 48 | // Pre-compile GraphQL queries 49 | ({ status } = cp.spawnSync('yarn', ['relay'], { stdio: 'inherit' })); 50 | if (status !== 0) process.exit(status); 51 | })().catch(async err => { 52 | console.error(err); 53 | if (db) await db.destroy(); 54 | process.exit(1); 55 | }); 56 | -------------------------------------------------------------------------------- /scripts/update-schema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | process.env.NODE_ENV = 'test'; 8 | 9 | require('@babel/register')({ 10 | babelrc: false, 11 | presets: [require.resolve('react-app-tools/config/babel')], 12 | }); 13 | 14 | const fs = require('fs'); 15 | const path = require('path'); 16 | const graphql = require('graphql'); 17 | const schema = require('../src/server/schema').default; 18 | const db = require('../src/server/db').default; 19 | 20 | fs.writeFileSync( 21 | path.resolve(__dirname, '../schema.graphql'), 22 | graphql.printSchema(schema, { commentDescriptions: true }), 23 | 'utf8', 24 | ); 25 | 26 | db.destroy(); 27 | -------------------------------------------------------------------------------- /seeds/seed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | const config = require('../knexfile'); 8 | 9 | module.exports.seed = async db => { 10 | for (const table of config.tables.slice().reverse()) { 11 | console.log(`Remove data from the ${table} table.`); 12 | await db.table(table).del(); 13 | } 14 | 15 | for (const table of config.tables) { 16 | console.log(`Seeding data into the ${table} table.`); 17 | const data = require(`./${table}.json`); 18 | if (table === 'user_identities') { 19 | data.forEach(x => { 20 | x.profile = JSON.stringify(x.profile); 21 | x.credentials = JSON.stringify(x.credentials); 22 | }); 23 | } 24 | await db.table(table).insert(data); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/admin/AdminLayout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { createFragmentContainer, graphql } from 'react-relay'; 9 | 10 | function AdminLayout({ children }) { 11 | return
{children}
; 12 | } 13 | 14 | export default createFragmentContainer(AdminLayout, { 15 | data: graphql` 16 | fragment AdminLayout_data on Query { 17 | me { 18 | id 19 | } 20 | } 21 | `, 22 | }); 23 | -------------------------------------------------------------------------------- /src/admin/AdminStoryList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import List from '@material-ui/core/List'; 10 | import ListItem from '@material-ui/core/ListItem'; 11 | import { createFragmentContainer, graphql } from 'react-relay'; 12 | 13 | function AdminStoryList(props) { 14 | return ( 15 | 16 | Stories 17 | 18 | Story A 19 | Story B 20 | Story C 21 | 22 | 23 | ); 24 | } 25 | 26 | export default createFragmentContainer(AdminStoryList, { 27 | data: graphql` 28 | fragment AdminStoryList_data on Query { 29 | me { 30 | id 31 | } 32 | } 33 | `, 34 | }); 35 | -------------------------------------------------------------------------------- /src/admin/AdminUserList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import List from '@material-ui/core/List'; 10 | import ListItem from '@material-ui/core/ListItem'; 11 | import { createFragmentContainer, graphql } from 'react-relay'; 12 | 13 | function AdminUserList(props) { 14 | return ( 15 | 16 | Users 17 | 18 | User A 19 | User B 20 | User C 21 | 22 | 23 | ); 24 | } 25 | 26 | export default createFragmentContainer(AdminUserList, { 27 | data: graphql` 28 | fragment AdminUserList_data on Query { 29 | me { 30 | id 31 | } 32 | } 33 | `, 34 | }); 35 | -------------------------------------------------------------------------------- /src/admin/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { graphql } from 'relay-runtime'; 9 | import AdminLayout from './AdminLayout'; 10 | import AdminUserList from './AdminUserList'; 11 | import AdminStoryList from './AdminStoryList'; 12 | 13 | export default [ 14 | { 15 | path: '/users', 16 | query: graphql` 17 | query adminUserListQuery { 18 | ...AdminLayout_data 19 | ...AdminUserList_data 20 | } 21 | `, 22 | render: (_, { data, users }) => ({ 23 | title: 'Manage Users', 24 | component: ( 25 | 26 | 27 | 28 | ), 29 | chunks: ['admin'], 30 | }), 31 | }, 32 | { 33 | path: '/stories', 34 | query: graphql` 35 | query adminStoryListQuery { 36 | ...AdminLayout_data 37 | ...AdminStoryList_data 38 | } 39 | `, 40 | render: (_, { data }) => ({ 41 | title: 'Manage Stories', 42 | component: ( 43 | 44 | 45 | 46 | ), 47 | chunks: ['admin'], 48 | }), 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /src/common/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import CssBaseline from '@material-ui/core/CssBaseline'; 9 | import { QueryRenderer } from 'react-relay'; 10 | import { ROOT_ID, REF_KEY } from 'relay-runtime'; 11 | import { MuiThemeProvider } from '@material-ui/core/styles'; 12 | 13 | import theme from '../theme'; 14 | import ErrorPage from './ErrorPage'; 15 | import LoginDialog from './LoginDialog'; 16 | import { gtag, getScrollPosition } from '../utils'; 17 | import { ConfigContext, HistoryContext, ResetContext } from '../hooks'; 18 | 19 | class App extends React.PureComponent { 20 | static getDerivedStateFromError(error) { 21 | return { error }; 22 | } 23 | 24 | componentDidMount() { 25 | this.componentDidRender(); 26 | } 27 | 28 | componentDidUpdate() { 29 | this.componentDidRender(); 30 | } 31 | 32 | componentDidCatch(error, info) { 33 | console.log(error, info); // eslint-disable-line no-console 34 | gtag('event', 'exception', { description: error.message, fatal: false }); 35 | } 36 | 37 | state = { error: null }; 38 | 39 | componentDidRender = () => { 40 | const { history, location, startTime, title, config, relay } = this.props; 41 | window.document.title = title; 42 | 43 | // Get the current user's ID 44 | const root = relay.getStore().getSource().get(ROOT_ID); // prettier-ignore 45 | const userId = root && root.me ? atob(root.me[REF_KEY]).substr(5) : ''; 46 | 47 | // Track page views, render time, etc. 48 | gtag('config', config.gaTrackingId, { 49 | transport_type: 'beacon', 50 | user_id: userId, 51 | }); 52 | gtag('event', 'timing_complete', { 53 | name: 'load', 54 | value: Math.round(performance.now() - startTime), 55 | event_category: 'Render Complete', 56 | }); 57 | 58 | const scrollY = getScrollPosition(location.key); 59 | 60 | if (scrollY && history.action === 'POP') { 61 | window.scrollTo(0, scrollY); 62 | } else { 63 | window.scrollTo(0, 0); 64 | } 65 | }; 66 | 67 | resetError = () => { 68 | this.setState({ error: null }); 69 | }; 70 | 71 | renderProps = ({ error, props }) => { 72 | const err = this.state.error || this.props.error || error; 73 | return err ? ( 74 | 75 | ) : props ? ( 76 | this.props.render(props || this.props.data) 77 | ) : null; 78 | }; 79 | 80 | render() { 81 | const { 82 | config, 83 | history, 84 | reset, 85 | relay, 86 | query, 87 | variables, 88 | payload, 89 | } = this.props; 90 | 91 | return ( 92 | 93 | 94 | 95 | 96 | 97 | 104 | 105 | 106 | 107 | 108 | 109 | ); 110 | } 111 | } 112 | 113 | export default App; 114 | -------------------------------------------------------------------------------- /src/common/App.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | it('renders without crashing', () => { 8 | // TODO: Implement a unit test 9 | }); 10 | -------------------------------------------------------------------------------- /src/common/AppBar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import clsx from 'clsx'; 8 | import React from 'react'; 9 | import Avatar from '@material-ui/core/Avatar'; 10 | import MuiAppBar from '@material-ui/core/AppBar'; 11 | import Toolbar from '@material-ui/core/Toolbar'; 12 | import Button from '@material-ui/core/Button'; 13 | import IconButton from '@material-ui/core/IconButton'; 14 | import CloseIcon from '@material-ui/icons/Close'; 15 | import { makeStyles } from '@material-ui/core/styles'; 16 | import { createFragmentContainer, graphql } from 'react-relay'; 17 | import { Typography } from '@material-ui/core'; 18 | 19 | import Link from './Link'; 20 | import UserMenu from './UserMenu'; 21 | import { useConfig, useHistory, useAuth } from '../hooks'; 22 | 23 | const useStyles = makeStyles(theme => ({ 24 | root: { 25 | backgroundColor: '#3f51b5', 26 | backgroundImage: 'linear-gradient(-225deg, #3db0ef, #5e5bb7)', 27 | }, 28 | title: { 29 | fontFamily: theme.typography.monoFamily, 30 | fontWeight: 300, 31 | fontSize: '1.25rem', 32 | }, 33 | titleLink: { 34 | color: 'inherit', 35 | textDecoration: 'none', 36 | }, 37 | avatarButton: { 38 | padding: theme.spacing(0.5), 39 | marginLeft: theme.spacing(1), 40 | }, 41 | avatar: { 42 | width: 32, 43 | height: 32, 44 | }, 45 | })); 46 | 47 | function AppBar(props) { 48 | const { 49 | className, 50 | me, 51 | relay, 52 | close, 53 | children, 54 | onOpenSettings, 55 | ...other 56 | } = props; 57 | const [userMenuEl, setUserMenuEl] = React.useState(null); 58 | const { app } = useConfig(); 59 | const history = useHistory(); 60 | const auth = useAuth(); 61 | const s = useStyles(); 62 | 63 | function handleClose() { 64 | history.replace('/'); 65 | } 66 | 67 | function openUserMenu(event) { 68 | setUserMenuEl(event.currentTarget); 69 | } 70 | 71 | function closeUserMenu() { 72 | setUserMenuEl(null); 73 | } 74 | 75 | function signIn() { 76 | closeUserMenu(); 77 | auth.signIn(); 78 | } 79 | 80 | return ( 81 | 82 | 83 | 84 | 85 | {app.name} 86 | 87 | 88 | 89 | {close ? ( 90 | 91 | 92 | 93 | ) : ( 94 | 95 | 98 | {children} 99 | {me && ( 100 | 106 | 111 | 112 | )} 113 | {me && ( 114 | 122 | )} 123 | {!me && ( 124 | 127 | )} 128 | 129 | )} 130 | 131 | 132 | ); 133 | } 134 | 135 | export default createFragmentContainer(AppBar, { 136 | me: graphql` 137 | fragment AppBar_me on User { 138 | id 139 | photoURL 140 | displayName 141 | } 142 | `, 143 | }); 144 | -------------------------------------------------------------------------------- /src/common/AutoUpdater.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { createFragmentContainer, commitMutation, graphql } from 'react-relay'; 9 | 10 | /** 11 | * Checks if user's profile settings (time zone etc.) are up-to-date, 12 | * and updates these fields in the background when they become outdated. 13 | */ 14 | class AutoUpdater extends React.Component { 15 | componentDidMount() { 16 | this.updateUser(); 17 | } 18 | 19 | shouldComponentUpdate({ me: next }) { 20 | const { me } = this.props; 21 | return !( 22 | (me && me.id) === (next && next.id) && 23 | (me && me.timeZone) === (next && next.timeZone) 24 | ); 25 | } 26 | 27 | componentDidUpdate() { 28 | this.updateUser(); 29 | } 30 | 31 | updateUser() { 32 | const { me, relay } = this.props; 33 | const { timeZone } = Intl.DateTimeFormat().resolvedOptions(); 34 | 35 | if (me && me.timeZone !== timeZone) { 36 | commitMutation(relay.environment, { 37 | mutation: graphql` 38 | mutation AutoUpdaterMutation($input: UpdateUserInput!) { 39 | updateUser(input: $input) { 40 | user { 41 | id 42 | timeZone 43 | } 44 | } 45 | } 46 | `, 47 | variables: { 48 | input: { id: me.id, timeZone }, 49 | }, 50 | }); 51 | } 52 | } 53 | 54 | render() { 55 | return null; 56 | } 57 | } 58 | 59 | export default createFragmentContainer(AutoUpdater, { 60 | me: graphql` 61 | fragment AutoUpdater_me on User { 62 | id 63 | timeZone 64 | } 65 | `, 66 | }); 67 | -------------------------------------------------------------------------------- /src/common/CustomerChat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { useTheme, makeStyles } from '@material-ui/core/styles'; 9 | import { useConfig, useFacebook } from '../hooks'; 10 | 11 | const useStyles = makeStyles(theme => ({ 12 | '@global': { 13 | '.fb_dialog,.fb_reset iframe': { 14 | zIndex: `${theme.zIndex.modal - 10} !important`, 15 | }, 16 | }, 17 | })); 18 | 19 | // https://developers.facebook.com/docs/messenger-platform/discovery/customer-chat-plugin 20 | const CustomerChat = React.memo(function CustomerChat() { 21 | const timeoutRef = React.useRef(); 22 | const config = useConfig(); 23 | const theme = useTheme(); 24 | useStyles(); 25 | 26 | // Initialize Facebook widget(s) in 2 seconds after 27 | // the component is mounted. 28 | useFacebook({ xfbml: false }, FB => { 29 | if (timeoutRef.current !== null) { 30 | timeoutRef.current = setTimeout(() => { 31 | const el = document.createElement('div'); 32 | el.className = 'fb-customerchat'; 33 | el.setAttribute('attribution', 'setup_tool'); 34 | el.setAttribute('page_id', config.facebook.pageId); 35 | el.setAttribute('ptheme_color', theme.palette.primary.main); 36 | // el.setAttribute('plogged_in_greeting', '...'); 37 | // el.setAttribute('plogged_out_greeting', '...'); 38 | // el.setAttribute('pgreeting_dialog_display', '...'); 39 | // el.setAttribute('pgreeting_dialog_delay', '...'); 40 | // el.setAttribute('pminimized', 'false'); 41 | document.body.appendChild(el); 42 | FB.XFBML.parse(); 43 | }, 2000); 44 | } 45 | }); 46 | 47 | return null; 48 | }); 49 | 50 | export default CustomerChat; 51 | -------------------------------------------------------------------------------- /src/common/ErrorPage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | 11 | import Link from './Link'; 12 | import { useHistory } from '../hooks'; 13 | 14 | const color = '#607d8b'; 15 | 16 | const useStyles = makeStyles(theme => ({ 17 | container: { 18 | position: 'absolute', 19 | top: 0, 20 | left: 0, 21 | display: 'flex', 22 | width: '100%', 23 | height: '100%', 24 | textAlign: 'center', 25 | justifyContent: 'center', 26 | alignItems: 'center', 27 | '@media only screen and (max-width: 280px)': { 28 | width: '95%', 29 | }, 30 | }, 31 | main: { 32 | paddingBottom: 80, 33 | marginLeft: theme.spacing(2), 34 | marginRight: theme.spacing(2), 35 | '@media screen and (max-width: 1024px)': { 36 | padding: theme.spacing(0, 1), 37 | }, 38 | }, 39 | errorCode: { 40 | margin: 0, 41 | fontSize: '15em', 42 | fontWeight: 300, 43 | lineHeight: 1, 44 | color, 45 | letterSpacing: '0.02em', 46 | '@media screen and (max-width: 1024px)': { 47 | fontSize: '10em', 48 | }, 49 | }, 50 | title: { 51 | paddingBottom: '0.5em', 52 | fontFamily: 'Roboto, Helvetica, Arial, sans-serif', 53 | fontSize: '2em', 54 | fontWeight: 400, 55 | lineHeight: '1em', 56 | color, 57 | letterSpacing: '0.02em', 58 | '@media only screen and (max-width: 280px)': { 59 | margin: '0 0 0.3em', 60 | fontSize: '1.5em', 61 | }, 62 | '@media screen and (max-width: 1024px)': { 63 | fontSize: '1.5em', 64 | }, 65 | }, 66 | text: { 67 | color: `color(${color} alpha(50%))`, 68 | '@media only screen and (max-width: 280px)': { 69 | width: '95%', 70 | }, 71 | }, 72 | link: { 73 | color: theme.palette.primary.main, 74 | }, 75 | })); 76 | 77 | function ErrorPage(props) { 78 | const history = useHistory(); 79 | const s = useStyles(); 80 | 81 | React.useEffect(() => { 82 | document.title = 83 | props.error && props.error.status === 404 ? 'Page Not Found' : 'Error'; 84 | }); 85 | 86 | function goBack(event) { 87 | event.preventDefault(); 88 | props.onClose(); 89 | history.goBack(); 90 | } 91 | 92 | if (props.error) { 93 | console.error(props.error); // eslint-disable-line no-console 94 | } 95 | 96 | const [code, title] = 97 | props.error && props.error.status === 404 98 | ? ['404', 'Page not found'] 99 | : ['Error', 'Oops, something went wrong']; 100 | 101 | return ( 102 |
103 |
104 | 105 | {code} 106 | 107 | 108 | {title} 109 | 110 | {code === '404' && ( 111 | 112 | The page you're looking for does not exist or an another error 113 | occurred. 114 |
115 |
116 | )} 117 | 118 | 119 | Go back 120 | 121 | , or head over to the  122 | 123 | home page 124 | {' '} 125 | to choose a new direction. 126 | 127 |
128 |
129 | ); 130 | } 131 | 132 | export default ErrorPage; 133 | -------------------------------------------------------------------------------- /src/common/Layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { makeStyles } from '@material-ui/core/styles'; 9 | import { createFragmentContainer, graphql } from 'react-relay'; 10 | 11 | import AppBar from './AppBar'; 12 | import LayoutFooter from './LayoutFooter'; 13 | import AutoUpdater from './AutoUpdater'; 14 | import UserSettingsDialog from './UserSettingsDialog'; 15 | 16 | const useStyles = makeStyles(theme => ({ 17 | background: { 18 | backgroundColor: '#3f51b5', 19 | backgroundImage: 'linear-gradient(-225deg, #3db0ef, #5e5bb7)', 20 | }, 21 | toolbar: { 22 | ...theme.mixins.toolbar, 23 | }, 24 | })); 25 | 26 | function Layout(props) { 27 | const { hero, data, children } = props; 28 | const [userSettings, setUserSettings] = React.useState({ open: false }); 29 | const s = useStyles(); 30 | 31 | function openUserSettings() { 32 | setUserSettings({ open: true, key: Date.now() }); 33 | } 34 | 35 | function closeUserSettings() { 36 | setUserSettings({ open: false }); 37 | } 38 | 39 | return ( 40 | 41 | 46 | {hero && ( 47 |
48 |
49 | {hero} 50 |
51 | )} 52 | {!hero &&
} 53 | {children} 54 | 55 | 56 | 62 | 63 | ); 64 | } 65 | 66 | export default createFragmentContainer(Layout, { 67 | data: graphql` 68 | fragment Layout_data on Query { 69 | me { 70 | ...AppBar_me 71 | ...AutoUpdater_me 72 | ...UserSettingsDialog_me 73 | } 74 | } 75 | `, 76 | }); 77 | -------------------------------------------------------------------------------- /src/common/LayoutFooter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | 11 | import Link from './Link'; 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | root: { 15 | ...theme.mixins.content, 16 | color: 'rgba(0, 0, 0, 0.4)', 17 | borderTop: `1px solid ${theme.palette.grey[300]}`, 18 | paddingTop: `${theme.spacing(2)}px !important`, 19 | }, 20 | text: {}, 21 | copyright: { 22 | paddingRight: '0.5em', 23 | }, 24 | separator: { 25 | paddingRight: '0.5em', 26 | paddingLeft: '0.5em', 27 | }, 28 | link: { 29 | color: 'rgba(0, 0, 0, 0.6)', 30 | textDecoration: 'none', 31 | '&:hover': { 32 | textDocoration: 'underline', 33 | }, 34 | }, 35 | })); 36 | 37 | function LayoutFooter() { 38 | const s = useStyles(); 39 | 40 | return ( 41 |
42 | 43 | © 2015-present 44 | 45 | Kriasoft 46 | 47 | | 48 | 49 | About Us 50 | 51 | | 52 | 53 | Terms 54 | 55 | | 56 | 57 | Privacy 58 | 59 | | 60 | 61 | Not Found 62 | 63 | 64 |
65 | ); 66 | } 67 | 68 | export default LayoutFooter; 69 | -------------------------------------------------------------------------------- /src/common/Link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { useHistory } from '../hooks'; 10 | 11 | function isLeftClickEvent(event) { 12 | return event.button === 0; 13 | } 14 | 15 | function isModifiedEvent(event) { 16 | return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); 17 | } 18 | 19 | const Link = React.forwardRef(function Link(props, ref) { 20 | const { state, ...other } = props; 21 | const history = useHistory(); 22 | 23 | function handleClick(event) { 24 | if (props.onClick) { 25 | props.onClick(event); 26 | } 27 | 28 | if (isModifiedEvent(event) || !isLeftClickEvent(event)) { 29 | return; 30 | } 31 | 32 | if (event.defaultPrevented === true) { 33 | return; 34 | } 35 | 36 | event.preventDefault(); 37 | history.push(event.currentTarget.getAttribute('href'), state); 38 | } 39 | 40 | // eslint-disable-next-line jsx-a11y/anchor-has-content 41 | return ; 42 | }); 43 | 44 | Link.propTypes = { 45 | state: PropTypes.instanceOf(Object), 46 | onClick: PropTypes.func, 47 | }; 48 | 49 | export default Link; 50 | -------------------------------------------------------------------------------- /src/common/LoginButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import clsx from 'clsx'; 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import Button from '@material-ui/core/Button'; 11 | import { darken } from '@material-ui/core/styles/colorManipulator'; 12 | import { withStyles } from '@material-ui/core/styles'; 13 | 14 | import FacebookIcon from '../icons/Facebook'; 15 | import GoogleIcon from '../icons/Google'; 16 | 17 | const providers = { 18 | facebook: { 19 | name: 'Facebook', 20 | icon: FacebookIcon, 21 | }, 22 | google: { 23 | name: 'Google', 24 | icon: GoogleIcon, 25 | }, 26 | }; 27 | 28 | const styles = { 29 | root: { 30 | fontSize: '1em', 31 | fontWeight: 100, 32 | textTransform: 'none', 33 | letterSpacing: 1, 34 | }, 35 | 36 | icon: { 37 | width: 24, 38 | height: 24, 39 | marginRight: '0.625em', 40 | 41 | '& path': { 42 | fill: '#fff', 43 | }, 44 | }, 45 | 46 | provider: { 47 | marginLeft: '0.375em', 48 | fontWeight: 400, 49 | }, 50 | 51 | facebook: { 52 | backgroundColor: 'rgb(66, 103, 178)', 53 | '&:hover': { 54 | backgroundColor: darken('rgb(66, 103, 178)', 0.1), 55 | }, 56 | }, 57 | 58 | google: { 59 | backgroundColor: 'rgb(66, 133, 244)', 60 | '&:hover': { 61 | backgroundColor: darken('rgb(66, 133, 244)', 0.1), 62 | }, 63 | }, 64 | }; 65 | 66 | class LoginButton extends React.PureComponent { 67 | static propTypes = { 68 | provider: PropTypes.oneOf(Object.keys(providers)), 69 | }; 70 | 71 | render() { 72 | const { className, classes: s, provider, ...rest } = this.props; 73 | const { name, icon: Icon } = providers[provider]; 74 | 75 | return ( 76 | 87 | ); 88 | } 89 | } 90 | 91 | export default withStyles(styles)(LoginButton); 92 | -------------------------------------------------------------------------------- /src/common/LoginDialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Dialog from '@material-ui/core/Dialog'; 9 | import DialogContent from '@material-ui/core/DialogContent'; 10 | 11 | import LoginForm from './LoginForm'; 12 | import { useAuth } from '../hooks/useAuth'; 13 | 14 | const LoginDialog = React.memo(function LoginDialog(props) { 15 | const [state, setState] = React.useState({}); 16 | useAuth({ onLogin: resolve => setState({ resolve }) }); 17 | 18 | function handleLoginComplete(user) { 19 | if (state.resolve) { 20 | state.resolve(user); 21 | handleClose(); 22 | } 23 | } 24 | 25 | function handleClose() { 26 | setState({}); 27 | } 28 | 29 | return ( 30 | 37 | 38 | 39 | 40 | 41 | ); 42 | }); 43 | 44 | export default LoginDialog; 45 | -------------------------------------------------------------------------------- /src/common/LoginForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import Button from '@material-ui/core/Button'; 10 | import Typography from '@material-ui/core/Typography'; 11 | import { makeStyles } from '@material-ui/core/styles'; 12 | 13 | import GoogleIcon from '../icons/Google'; 14 | import FacebookIcon from '../icons/Facebook'; 15 | import { useAuth } from '../hooks/useAuth'; 16 | 17 | const useStyles = makeStyles(theme => ({ 18 | button: { 19 | marginLeft: theme.spacing(0.5), 20 | marginRight: theme.spacing(0.5), 21 | }, 22 | })); 23 | 24 | const providers = [ 25 | { id: 'google', name: 'Google', icon: GoogleIcon }, 26 | { id: 'facebook', name: 'Facebook', icon: FacebookIcon }, 27 | ]; 28 | 29 | function LoginForm(props) { 30 | const { onLoginComplete, ...other } = props; 31 | const s = useStyles(); 32 | const auth = useAuth(); 33 | 34 | function signIn(event) { 35 | const { provider } = event.currentTarget.dataset; 36 | auth.signInWith(provider).then(user => { 37 | onLoginComplete(user); 38 | }); 39 | } 40 | 41 | return ( 42 |
43 | 44 | Sign in or register with your social media account 45 | 46 | 47 | {providers.map(x => ( 48 | 59 | ))} 60 | 61 |
62 | ); 63 | } 64 | 65 | LoginForm.propTypes = { 66 | onLoginComplete: PropTypes.func.isRequired, 67 | }; 68 | 69 | export default LoginForm; 70 | -------------------------------------------------------------------------------- /src/common/TextField.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import clsx from 'clsx'; 8 | import React from 'react'; 9 | import MuiTextField from '@material-ui/core/TextField'; 10 | import { makeStyles } from '@material-ui/core/styles'; 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | root: { 14 | marginBottom: theme.spacing(1), 15 | }, 16 | maxLength: { 17 | float: 'right', 18 | }, 19 | })); 20 | 21 | function TextField(props) { 22 | const { 23 | className, 24 | name, 25 | state: [state, setState], 26 | maxLength, 27 | required, 28 | ...other 29 | } = props; 30 | 31 | const s = useStyles(); 32 | const errors = (state.errors || {})[name] || []; 33 | const error = errors.length ? errors.join(' \n') : null; 34 | const hasError = 35 | Boolean(error) || (maxLength && (state[name] || '').length > maxLength); 36 | 37 | let helperText = error || (required ? '* Required' : ' '); 38 | 39 | if (maxLength) { 40 | helperText = ( 41 | 42 | {helperText} 43 | 44 | {(state[name] || '').length}/{maxLength} 45 | 46 | 47 | ); 48 | } 49 | 50 | function handleChange(event) { 51 | const { name, value } = event.target; 52 | setState(x => ({ ...x, [name]: value })); 53 | } 54 | 55 | return ( 56 | 65 | ); 66 | } 67 | 68 | export default TextField; 69 | -------------------------------------------------------------------------------- /src/common/UserMenu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import clsx from 'clsx'; 8 | import React from 'react'; 9 | import Menu from '@material-ui/core/Menu'; 10 | import MenuItem from '@material-ui/core/MenuItem'; 11 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 12 | import ListItemText from '@material-ui/core/ListItemText'; 13 | import { makeStyles } from '@material-ui/core/styles'; 14 | 15 | import Settings from '../icons/Settings'; 16 | import Logout from '../icons/Logout'; 17 | import { useAuth } from '../hooks'; 18 | 19 | const useStyles = makeStyles(theme => ({ 20 | list: { 21 | minWidth: 140, 22 | }, 23 | icon: { 24 | minWidth: 32, 25 | }, 26 | })); 27 | 28 | function UserMenu(props) { 29 | const { className, onOpenSettings, ...other } = props; 30 | const open = Boolean(props.anchorEl); 31 | const auth = useAuth(); 32 | const s = useStyles(); 33 | 34 | const openSettings = React.useCallback(() => { 35 | props.onClose(); 36 | onOpenSettings(); 37 | }, [props.onClose, onOpenSettings]); 38 | 39 | function signOut() { 40 | props.onClose(); 41 | auth.signOut(); 42 | } 43 | 44 | return ( 45 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | 78 | export default UserMenu; 79 | -------------------------------------------------------------------------------- /src/common/UserSettingsDialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Dialog from '@material-ui/core/Dialog'; 9 | import DialogTitle from '@material-ui/core/DialogTitle'; 10 | import DialogContent from '@material-ui/core/DialogContent'; 11 | import DialogActions from '@material-ui/core/DialogActions'; 12 | import TextField from '@material-ui/core/TextField'; 13 | import Button from '@material-ui/core/Button'; 14 | import { makeStyles } from '@material-ui/core/styles'; 15 | import { createRefetchContainer, graphql } from 'react-relay'; 16 | 17 | import UpdateUserMutation from '../mutations/UpdateUser'; 18 | 19 | const useStyles = makeStyles(theme => ({ 20 | field: { 21 | '&:not(:last-child)': { 22 | marginBottom: theme.spacing(1), 23 | }, 24 | }, 25 | })); 26 | 27 | function UserSettingsDialog(props) { 28 | const { me, relay, ...other } = props; 29 | const [state, setState] = React.useState({ errors: {} }); 30 | const s = useStyles(); 31 | 32 | React.useEffect(() => { 33 | relay.refetch({ mounted: true }); 34 | }, []); 35 | 36 | React.useEffect(() => { 37 | setState({ ...me, errors: {} }); 38 | }, [JSON.stringify(me)]); 39 | 40 | function handleChange(event) { 41 | const { name, value } = event.target; 42 | setState(x => ({ ...x, [name]: value })); 43 | } 44 | 45 | function handleSubmit(event) { 46 | event.preventDefault(); 47 | setState(x => (x.errors ? { ...x, errors: {} } : x)); 48 | 49 | UpdateUserMutation.commit( 50 | relay.environment, 51 | { 52 | id: state.id, 53 | displayName: state.displayName, 54 | email: state.email, 55 | }, 56 | errors => { 57 | if (errors) { 58 | setState(x => ({ ...x, errors })); 59 | } else { 60 | props.onClose(); 61 | } 62 | }, 63 | ); 64 | } 65 | 66 | return ( 67 | 68 | User Settings 69 | 70 |
71 | 86 | 98 | 99 |
100 | 101 | 102 | 105 | 106 |
107 | ); 108 | } 109 | 110 | export default createRefetchContainer( 111 | UserSettingsDialog, 112 | { 113 | me: graphql` 114 | fragment UserSettingsDialog_me on User 115 | @argumentDefinitions( 116 | mounted: { type: "Boolean", defaultValue: false } 117 | ) { 118 | id 119 | photoURL @include(if: $mounted) 120 | displayName @include(if: $mounted) 121 | email @include(if: $mounted) 122 | } 123 | `, 124 | }, 125 | graphql` 126 | query UserSettingsDialogQuery($mounted: Boolean!) { 127 | me { 128 | ...UserSettingsDialog_me @arguments(mounted: $mounted) 129 | } 130 | } 131 | `, 132 | ); 133 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | export * from './useAuth'; 8 | export * from './useConfig'; 9 | export * from './useFacebook'; 10 | export * from './useFirebase'; 11 | export * from './useGoogleMaps'; 12 | export * from './useHistory'; 13 | export * from './useRelay'; 14 | export * from './useReset'; 15 | -------------------------------------------------------------------------------- /src/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { commitLocalUpdate } from 'relay-runtime'; 9 | import { useHistory } from './useHistory'; 10 | import { useRelay } from './useRelay'; 11 | import { useReset } from './useReset'; 12 | 13 | const WINDOW_WIDTH = 600; 14 | const WINDOW_HEIGHT = 600; 15 | 16 | let loginWindow; 17 | 18 | function openLoginWindow(url) { 19 | if (loginWindow && !loginWindow.closed) { 20 | loginWindow.location.href = url; 21 | loginWindow.focus(); 22 | } else { 23 | const { screenLeft, screenTop, innerWidth, innerHeight, screen } = window; 24 | const html = window.document.documentElement; 25 | 26 | const dualScreenLeft = screenLeft !== undefined ? screenLeft : screen.left; 27 | const dualScreenTop = screenTop !== undefined ? screenTop : screen.top; 28 | const w = innerWidth || html.clientWidth || screen.width; 29 | const h = innerHeight || html.clientHeight || screen.height; 30 | 31 | const config = { 32 | width: WINDOW_WIDTH, 33 | height: WINDOW_HEIGHT, 34 | left: w / 2 - WINDOW_WIDTH / 2 + dualScreenLeft, 35 | top: h / 2 - WINDOW_HEIGHT / 2 + dualScreenTop, 36 | }; 37 | 38 | loginWindow = window.open( 39 | url, 40 | null, 41 | Object.keys(config) 42 | .map(key => `${key}=${config[key]}`) 43 | .join(','), 44 | ); 45 | } 46 | } 47 | 48 | const onLoginCallbacks = new Set(); 49 | 50 | function signIn() { 51 | return new Promise(resolve => { 52 | onLoginCallbacks.forEach(cb => cb(resolve)); 53 | }); 54 | } 55 | 56 | export function useAuth(options) { 57 | const relayRef = React.useRef(); 58 | const callbacks = React.useRef([]); 59 | const history = useHistory(); 60 | const relay = useRelay(); 61 | const reset = useReset(); 62 | 63 | React.useEffect(() => { 64 | relayRef.current = relay && relay.environment; 65 | }, [relay && relay.environment]); 66 | 67 | React.useEffect(() => { 68 | if (options && options.onLogin) { 69 | onLoginCallbacks.add(options.onLogin); 70 | } 71 | 72 | function handleMessage({ origin, data }) { 73 | if (origin === window.location.origin && data.type === 'LOGIN') { 74 | if (!data.error && relayRef.current) { 75 | const user = data.user; 76 | if (user && user.id) { 77 | commitLocalUpdate(relayRef.current, store => { 78 | const me = store.get(user.id) || store.create(user.id, 'User'); 79 | Object.keys(user).forEach(key => { 80 | me.setValue(user[key], key); 81 | }); 82 | store.getRoot().setLinkedRecord(me, 'me'); 83 | }); 84 | } 85 | } 86 | 87 | callbacks.current.forEach(cb => 88 | data.error ? cb[1](data.error) : cb[0](data.user), 89 | ); 90 | callbacks.current = []; 91 | } 92 | } 93 | 94 | window.addEventListener('message', handleMessage, true); 95 | return () => { 96 | window.removeEventListener('message', handleMessage); 97 | 98 | if (options && options.onLogin) { 99 | onLoginCallbacks.delete(options.onLogin); 100 | } 101 | }; 102 | }, []); 103 | 104 | const signInWith = React.useCallback( 105 | provider => { 106 | openLoginWindow(`/login/${provider}`); 107 | return new Promise((resolve, reject) => { 108 | callbacks.current.push([resolve, reject]); 109 | }).then(user => { 110 | reset(); 111 | history.replace(history.location); 112 | return user; 113 | }); 114 | }, 115 | [reset], 116 | ); 117 | 118 | const signOut = React.useCallback(() => { 119 | fetch('/login/clear', { 120 | method: 'POST', 121 | credentials: 'include', 122 | }).then(() => { 123 | reset(); 124 | history.push('/'); 125 | }); 126 | }, [reset]); 127 | 128 | return React.useMemo(() => ({ signIn, signInWith, signOut }), [ 129 | signIn, 130 | signInWith, 131 | signOut, 132 | ]); 133 | } 134 | -------------------------------------------------------------------------------- /src/hooks/useConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | export const ConfigContext = React.createContext({}); 10 | 11 | export function useConfig() { 12 | return React.useContext(ConfigContext); 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/useFacebook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | let promise; 10 | 11 | export function useFacebook(options, cb) { 12 | if (typeof options === 'function') { 13 | cb = options; 14 | } 15 | 16 | React.useEffect(() => { 17 | if (promise) { 18 | promise.then(cb); 19 | } else { 20 | promise = new Promise(resolve => { 21 | // https://developers.facebook.com/docs/javascript/reference/FB.init 22 | window.fbAsyncInit = () => { 23 | window.FB.init({ 24 | appId: window.config.facebook.appId, 25 | autoLogAppEvents: true, 26 | status: true, 27 | cookie: true, 28 | xfbml: true, 29 | version: 'v5.0', 30 | ...options, 31 | }); 32 | resolve(window.FB); 33 | }; 34 | 35 | const script = document.createElement('script'); 36 | const isDebug = window.localStorage.getItem('fb:debug') === 'true'; 37 | script.src = `https://connect.facebook.net/en_US/sdk/xfbml.customerchat${isDebug ? '/debug' : ''}.js`; // prettier-ignore 38 | document.head.appendChild(script); 39 | }); 40 | promise.then(cb); 41 | } 42 | }, []); 43 | } 44 | -------------------------------------------------------------------------------- /src/hooks/useFirebase.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { fetchQuery, graphql } from 'react-relay'; 9 | import { useRelay } from './useRelay'; 10 | 11 | let promise; 12 | let state = [null /* firebase client */, undefined /* firebase user */]; 13 | let listeners = new Set(); 14 | 15 | export function useFirebase() { 16 | const relay = useRelay(); 17 | const [, setState] = React.useState(state); 18 | 19 | React.useEffect(() => { 20 | listeners.add(setState); 21 | 22 | function authenticate(firebase) { 23 | // Fetch Firebase authentication token from our API 24 | const query = graphql` 25 | query useFirebaseQuery { 26 | me { 27 | firebaseToken 28 | } 29 | } 30 | `; 31 | 32 | fetchQuery(relay.environment, query, {}).then(({ me }) => { 33 | if ((me || {}).firebaseToken) { 34 | firebase 35 | .auth() 36 | .signInWithCustomToken((me || {}).firebaseToken) 37 | .catch(console.error); 38 | } else { 39 | firebase.auth().signOut(); 40 | } 41 | }); 42 | 43 | return firebase; 44 | } 45 | 46 | if (promise) { 47 | promise = promise.then(authenticate); 48 | } else { 49 | promise = Promise.all([ 50 | import(/* webpackChunkName: 'firebase' */ '@firebase/app'), 51 | import(/* webpackChunkName: 'firebase' */ '@firebase/auth'), 52 | ]).then(([{ default: firebase }]) => { 53 | // Initialize Firebase Client SDK 54 | if (!firebase.apps.length) { 55 | firebase.initializeApp(window.config.firebase); 56 | firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE); 57 | } 58 | 59 | // Save the current Firebase user to the component's state 60 | firebase.auth().onAuthStateChanged(user => { 61 | if ( 62 | JSON.stringify(state[1] && state[1].toJSON()) !== 63 | JSON.stringify(user && user.toJSON()) || 64 | state[0] !== firebase 65 | ) { 66 | state = [firebase, user]; 67 | listeners.forEach(x => x(state)); 68 | } 69 | }); 70 | 71 | return authenticate(firebase); 72 | }); 73 | } 74 | 75 | return () => listeners.delete(setState); 76 | }, []); 77 | 78 | return state; 79 | } 80 | -------------------------------------------------------------------------------- /src/hooks/useGoogleMaps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { useConfig } from './useConfig'; 9 | 10 | let promise; 11 | 12 | export function useGoogleMaps(cb) { 13 | const { gcpServiceKey: key } = useConfig(); 14 | 15 | React.useEffect(() => { 16 | if (promise) { 17 | promise.then(cb); 18 | } else { 19 | promise = new Promise(resolve => { 20 | window.initGoogleMaps = () => resolve(window.google.maps); 21 | const script = document.createElement('script'); 22 | script.src = `https://maps.googleapis.com/maps/api/js?key=${key}&libraries=places&callback=initGoogleMaps`; // prettier-ignore 23 | document.head.appendChild(script); 24 | }); 25 | promise.then(cb); 26 | } 27 | }, []); 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/useHistory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | // Provide default history object (for unit testing) 10 | export const HistoryContext = React.createContext({ 11 | location: { pathname: '/' }, 12 | }); 13 | 14 | export function useHistory() { 15 | return React.useContext(HistoryContext); 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/useRelay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { ReactRelayContext } from 'react-relay'; 9 | 10 | export function useRelay() { 11 | return React.useContext(ReactRelayContext); 12 | } 13 | -------------------------------------------------------------------------------- /src/hooks/useReset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | export const ResetContext = React.createContext(() => {}); 10 | 11 | export function useReset() { 12 | return React.useContext(ResetContext); 13 | } 14 | -------------------------------------------------------------------------------- /src/icons/Facebook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import SvgIcon from '@material-ui/core/SvgIcon'; 9 | 10 | const Facebook = React.forwardRef(function Facebook(props, ref) { 11 | const { size = 256, ...other } = props; 12 | return ( 13 | 22 | 23 | 27 | 28 | ); 29 | }); 30 | 31 | export default Facebook; 32 | -------------------------------------------------------------------------------- /src/icons/GitHub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import SvgIcon from '@material-ui/core/SvgIcon'; 9 | 10 | const GitHub = React.forwardRef(function GitHub(props, ref) { 11 | return ( 12 | 19 | 20 | 21 | ); 22 | }); 23 | 24 | export default GitHub; 25 | -------------------------------------------------------------------------------- /src/icons/Google.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import SvgIcon from '@material-ui/core/SvgIcon'; 9 | 10 | const Google = React.forwardRef(function Google(props, ref) { 11 | const { size = 256, ...other } = props; 12 | return ( 13 | 21 | 22 | 26 | 30 | 34 | 38 | 39 | 40 | ); 41 | }); 42 | 43 | export default Google; 44 | -------------------------------------------------------------------------------- /src/icons/Instagram.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import SvgIcon from '@material-ui/core/SvgIcon'; 9 | 10 | const Instagram = React.forwardRef(function Instagram(props, ref) { 11 | const { size = 24, ...other } = props; 12 | return ( 13 | 22 | 23 | 24 | ); 25 | }); 26 | 27 | export default Instagram; 28 | -------------------------------------------------------------------------------- /src/icons/Logout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import SvgIcon from '@material-ui/core/SvgIcon'; 9 | 10 | const Logout = React.forwardRef(function Logout(props, ref) { 11 | const { size = 24, ...other } = props; 12 | return ( 13 | 21 | 22 | 23 | ); 24 | }); 25 | 26 | export default Logout; 27 | -------------------------------------------------------------------------------- /src/icons/Settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import SvgIcon from '@material-ui/core/SvgIcon'; 9 | 10 | const Settings = React.forwardRef(function Settings(props, ref) { 11 | const { size = 24, ...other } = props; 12 | return ( 13 | 21 | 22 | 23 | ); 24 | }); 25 | 26 | export default Settings; 27 | -------------------------------------------------------------------------------- /src/icons/Twitter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import SvgIcon from '@material-ui/core/SvgIcon'; 9 | 10 | const Twitter = React.forwardRef(function Twitter(props, ref) { 11 | const { size = 24, ...other } = props; 12 | return ( 13 | 22 | Twitter icon 23 | 24 | 25 | ); 26 | }); 27 | 28 | export default Twitter; 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom'; 9 | import qs from 'query-string'; 10 | import { createBrowserHistory } from 'history'; 11 | 12 | import App from './common/App'; 13 | import * as serviceWorker from './serviceWorker'; 14 | import router from './router'; 15 | import { createRelay } from './relay'; 16 | import { setHistory } from './utils/scrolling'; 17 | 18 | const container = document.getElementById('root'); 19 | const history = createBrowserHistory(); 20 | 21 | let relay = createRelay(); 22 | 23 | setHistory(history); 24 | 25 | function reset() { 26 | relay = createRelay(); 27 | window.sessionStorage.removeItem('returnTo'); 28 | } 29 | 30 | function render(location) { 31 | const startTime = performance.now(); 32 | router 33 | .resolve({ 34 | pathname: location.pathname, 35 | query: qs.parse(location.search), 36 | relay, 37 | config: window.config, 38 | }) 39 | .then(route => { 40 | if (route.redirect) { 41 | history.replace(route.redirect); 42 | } else { 43 | ReactDOM.render( 44 | , 53 | container, 54 | ); 55 | } 56 | }); 57 | } 58 | 59 | history.listen(render); 60 | render(history.location); 61 | 62 | // If you want your app to work offline and load faster, you can change 63 | // unregister() to register() below. Note this comes with some pitfalls. 64 | // Learn more about service workers: http://bit.ly/CRA-PWA 65 | serviceWorker.unregister(); 66 | 67 | // Hot Module Replacement 68 | // https://webpack.js.org/guides/hot-module-replacement/ 69 | if (module.hot) { 70 | module.hot.accept('./router', () => { 71 | render(history.location); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/landing/Home.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Paper from '@material-ui/core/Paper'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import { makeStyles } from '@material-ui/core/styles'; 11 | 12 | import HomeSponsors from './HomeSponsors'; 13 | import HomeStack from './HomeStack'; 14 | 15 | const useStyles = makeStyles(theme => ({ 16 | content: { 17 | ...theme.mixins.content, 18 | }, 19 | title: { 20 | textAlign: 'center', 21 | }, 22 | subTitle: { 23 | textAlign: 'center', 24 | }, 25 | code: { 26 | padding: theme.spacing(2), 27 | color: theme.palette.common.white, 28 | backgroundColor: '#555', 29 | fontFamily: '"Roboto Mono"', 30 | fontWeight: 100, 31 | fontSize: '0.875rem', 32 | marginBottom: theme.spacing(3), 33 | }, 34 | block: {}, 35 | })); 36 | 37 | function Home() { 38 | const s = useStyles(); 39 | 40 | return ( 41 | 42 | 43 |
44 | 45 | Getting Started 46 | 47 | 48 | Just clone the{' '} 49 | 54 | repository 55 | 56 | , tweak environment variables found in .env.* files in the root of the 57 | project and start hacking. 58 | 59 | 60 | $ git clone https://github.com/kriasoft/react-firebase-starter.git 61 | example 62 |
63 | $ cd ./example 64 |
65 | $ yarn setup 66 |
$ yarn start 67 |
68 | 69 | Tech Stack 70 | 71 | 72 | Save time. Create with confidence. 73 | 74 | 75 |
76 | 77 | ); 78 | } 79 | 80 | export default Home; 81 | -------------------------------------------------------------------------------- /src/landing/HomeHero.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | 11 | import LoginButton from '../common/LoginButton'; 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | root: { 15 | ...theme.mixins.content, 16 | paddingTop: '2rem', 17 | paddingBottom: '3rem', 18 | [theme.breakpoints.up('sm')]: { 19 | paddingTop: '3rem', 20 | paddingBottom: '4rem', 21 | }, 22 | }, 23 | title: { 24 | paddingBottom: '1rem', 25 | fontWeight: 300, 26 | fontSize: '1.75rem', 27 | color: theme.palette.common.white, 28 | [theme.breakpoints.up('sm')]: { 29 | fontSize: '2.5rem', 30 | }, 31 | }, 32 | subTitle: { 33 | paddingBottom: '1rem', 34 | color: theme.palette.common.white, 35 | fontWeight: 300, 36 | fontSize: '1.125rem', 37 | [theme.breakpoints.up('sm')]: { 38 | fontSize: '1.5rem', 39 | }, 40 | }, 41 | actions: { 42 | paddingTop: '1rem', 43 | }, 44 | button: { 45 | boxShadow: 'none', 46 | backgroundColor: '#555', 47 | '&:hover': { 48 | backgroundColor: '#666', 49 | }, 50 | }, 51 | })); 52 | 53 | function HomeHero() { 54 | const s = useStyles(); 55 | 56 | return ( 57 |
58 | 59 | Flying start for makers 60 | 61 | 62 | Quickly bootstrap new web application projects on a solid 63 | JavaScript-based tech stack and serverless architecture 64 | 65 |
66 | 67 |
68 |
69 | ); 70 | } 71 | 72 | export default HomeHero; 73 | -------------------------------------------------------------------------------- /src/landing/HomeSponsors.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | 4 | const sponsors = [ 5 | { 6 | name: 'Rollbar', 7 | link: 8 | 'https://rollbar.com/?utm_source=reactstartkit(github)&utm_medium=link&utm_campaign=reactstartkit(github)', 9 | image: { 10 | src: 'https://files.tarkus.me/rollbar-384x64.png', 11 | width: 192, 12 | height: 32, 13 | }, 14 | }, 15 | { 16 | name: 'DigitalOcean', 17 | link: 18 | 'https://www.digitalocean.com/?refcode=eef302dbae9f&utm_source=github&utm_medium=oss_sponsorships&utm_campaign=opencollective', 19 | image: { 20 | src: 'https://files.tarkus.me/digital-ocean-393x64.png', 21 | width: 196.5, 22 | height: 32, 23 | }, 24 | }, 25 | ]; 26 | 27 | const useStyles = makeStyles(theme => ({ 28 | root: { 29 | display: 'flex', 30 | justifyContent: 'center', 31 | background: theme.palette.background.paper, 32 | padding: theme.spacing(2), 33 | }, 34 | link: { 35 | margin: theme.spacing(1), 36 | }, 37 | })); 38 | 39 | function HomeSponsors() { 40 | const s = useStyles(); 41 | 42 | return ( 43 |
44 | {sponsors.map(x => ( 45 | 52 | {x.name} 53 | 54 | ))} 55 |
56 | ); 57 | } 58 | 59 | export default HomeSponsors; 60 | -------------------------------------------------------------------------------- /src/landing/HomeStack.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grid from '@material-ui/core/Grid'; 3 | import Typography from '@material-ui/core/Typography'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | 6 | const useStyles = makeStyles(theme => ({ 7 | root: {}, 8 | item: { 9 | textAlign: 'center', 10 | }, 11 | title: {}, 12 | desc: { 13 | textAlign: 'left', 14 | }, 15 | })); 16 | 17 | const links = new Map([ 18 | ['Knex.js', 'https://knexjs.org/'], 19 | ['GraphQL.js', 'https://github.com/graphql/graphql-js'], 20 | ['React.js', 'https://reactjs.org/'], 21 | ['Relay', 'https://facebook.github.io/relay'], 22 | ['Material UI', 'https://material-ui.com/'], 23 | ]); 24 | 25 | function ExtLink(props) { 26 | return ( 27 | // eslint-disable-next-line jsx-a11y/anchor-has-content 28 | 34 | ); 35 | } 36 | 37 | function HomeStack() { 38 | const s = useStyles(); 39 | 40 | return ( 41 | 42 | 43 | 44 | GraphQL API 45 | 46 | 47 | Everything that you need for building an API with{' '} 48 | Knex.js and GraphQL.js. Check 49 | out GraphiQL IDE and{' '} 50 | data model. 51 | 52 | 53 | 54 | 55 | React.js and Relay 56 | 57 | 58 | No frameworks! Just vanilla JavaScript, React.js{' '} 59 | and Relay libraries that are proven to work great 60 | for building modern scalable web apps. 61 | 62 | 63 | 64 | 65 | Material UI 66 | 67 | 68 | Material UI is by far the most popular UI library 69 | on React.js stack that follows Google's 70 | Material Design guidelines. 71 | 72 | 73 | 74 | ); 75 | } 76 | 77 | export default HomeStack; 78 | -------------------------------------------------------------------------------- /src/landing/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { graphql } from 'relay-runtime'; 9 | import Layout from '../common/Layout'; 10 | 11 | export default [ 12 | { 13 | path: '', 14 | query: graphql` 15 | query landingHomeQuery { 16 | ...Layout_data 17 | } 18 | `, 19 | components: () => [ 20 | import(/* webpackChunkName: 'home' */ './Home'), 21 | import(/* webpackChunkName: 'home' */ './HomeHero'), 22 | ], 23 | render: ([Home, HomeHero], data, { config }) => ({ 24 | title: config.app.name, 25 | component: ( 26 | }> 27 | 28 | 29 | ), 30 | chunks: ['home'], 31 | }), 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /src/legal/Privacy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | import { useConfig } from '../hooks'; 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | root: { 14 | ...theme.mixins.content, 15 | }, 16 | })); 17 | 18 | function Privacy() { 19 | const s = useStyles(); 20 | const { app } = useConfig(); 21 | 22 | return ( 23 |
24 | 25 | Privacy Policy 26 | 27 | 28 | Your privacy is important to us. It is Company's policy to respect 29 | your privacy regarading any information we may collect from you across 30 | our website, {app.origin}, and other 31 | sites we own and operate. 32 | 33 | 34 | We only ask for personal information when we truly need it to provide a 35 | service to you. We collect it by fair and lawful means, with your 36 | knowledge and consent. We also let you know why we’re collecting it and 37 | how it will be used. 38 | 39 | 40 | We only retain collected information for as long as necessary to provide 41 | you with your requested service. What data we store, we’ll protect 42 | within commercially acceptable means to prevent loss and theft, as well 43 | as unauthorised access, disclosure, copying, use or modification. 44 | 45 | 46 | We don’t share any personally identifying information publicly or with 47 | third-parties, except when required to by law. 48 | 49 | 50 | Our website may link to external sites that are not operated by us. 51 | Please be aware that we have no control over the content and practices 52 | of these sites, and cannot accept responsibility or liability for their 53 | respective privacy policies. 54 | 55 | 56 | You are free to refuse our request for your personal information, with 57 | the understanding that we may be unable to provide you with some of your 58 | desired services. 59 | 60 | 61 | Your continued use of our website will be regarded as acceptance of our 62 | practices around privacy and personal information. If you have any 63 | questions about how we handle user data and personal information, feel 64 | free to contact us. 65 | 66 | 67 | This policy is effective as of January 1st, 2019. 68 | 69 |
70 | ); 71 | } 72 | 73 | export default Privacy; 74 | -------------------------------------------------------------------------------- /src/legal/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { graphql } from 'relay-runtime'; 9 | import Layout from '../common/Layout'; 10 | 11 | export default [ 12 | { 13 | path: '/terms', 14 | query: graphql` 15 | query legalTermsQuery { 16 | ...Layout_data 17 | } 18 | `, 19 | components: () => [import(/* webpackChunkName: 'terms' */ './Terms')], 20 | render: ([Terms], data, { config }) => ({ 21 | title: `Terms of Use • ${config.app.name}`, 22 | component: ( 23 | 24 | 25 | 26 | ), 27 | chunks: ['terms'], 28 | }), 29 | }, 30 | { 31 | path: '/privacy', 32 | query: graphql` 33 | query legalPrivacyQuery { 34 | ...Layout_data 35 | } 36 | `, 37 | components: () => [import(/* webpackChunkName: 'privacy' */ './Privacy')], 38 | render: ([Privacy], data, { config }) => ({ 39 | title: `Privacy Policy • ${config.app.name}`, 40 | component: ( 41 | 42 | 43 | 44 | ), 45 | chunks: ['privacy'], 46 | }), 47 | }, 48 | ]; 49 | -------------------------------------------------------------------------------- /src/misc/About.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | 11 | const useStyles = makeStyles(theme => ({ 12 | root: { 13 | ...theme.mixins.content, 14 | }, 15 | })); 16 | 17 | function About() { 18 | const s = useStyles(); 19 | 20 | return ( 21 |
22 | 23 | About Us 24 | 25 | 26 | Lorem Ipsum is simply dummy text of the printing and typesetting 27 | industry. Lorem Ipsum has been the industry's standard dummy text 28 | ever since the 1500s, when an unknown printer took a galley of type and 29 | scrambled it to make a type specimen book. It has survived not only five 30 | centuries, but also the leap into electronic typesetting, remaining 31 | essentially unchanged. It was popularised in the 1960s with the release 32 | of Letraset sheets containing Lorem Ipsum passages, and more recently 33 | with desktop publishing software like Aldus PageMaker including versions 34 | of Lorem Ipsum. 35 | 36 |
37 | ); 38 | } 39 | 40 | export default About; 41 | -------------------------------------------------------------------------------- /src/misc/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { graphql } from 'relay-runtime'; 9 | import Layout from '../common/Layout'; 10 | 11 | export default [ 12 | { 13 | path: '/about', 14 | query: graphql` 15 | query miscAboutQuery { 16 | ...Layout_data 17 | } 18 | `, 19 | components: () => [import(/* webpackChunkName: 'about' */ './About')], 20 | render: ([About], data, { config }) => ({ 21 | title: `About Us • ${config.app.name}`, 22 | component: ( 23 | 24 | 25 | 26 | ), 27 | chunks: ['about'], 28 | }), 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /src/mutations/DeleteUser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { commitMutation, graphql } from 'react-relay'; 8 | 9 | function commit(environment, id) { 10 | return commitMutation(environment, { 11 | mutation: graphql` 12 | mutation DeleteUserMutation($input: DeleteUserInput!) { 13 | deleteUser(input: $input) { 14 | clientMutationId 15 | deletedUserId 16 | } 17 | } 18 | `, 19 | 20 | variables: { input: { id } }, 21 | }); 22 | } 23 | 24 | export default { commit }; 25 | -------------------------------------------------------------------------------- /src/mutations/LikeStory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { commitMutation, graphql } from 'react-relay'; 8 | 9 | function commit(environment, storyId, done) { 10 | return commitMutation(environment, { 11 | mutation: graphql` 12 | mutation LikeStoryMutation($input: LikeStoryInput!) { 13 | likeStory(input: $input) { 14 | story { 15 | id 16 | pointsCount 17 | pointGiven 18 | } 19 | } 20 | } 21 | `, 22 | 23 | variables: { input: { id: storyId } }, 24 | 25 | onCompleted({ likeStory }, errors) { 26 | if (done) { 27 | done((errors && errors[0]) || null, likeStory && likeStory.story); 28 | } 29 | }, 30 | 31 | optimisticUpdater(store) { 32 | const story = store.get(storyId); 33 | 34 | const pointsCount = story.getValue('pointsCount'); 35 | const pointGiven = story.getValue('pointGiven'); 36 | 37 | story.setValue(pointsCount + (pointGiven ? -1 : 1), 'pointsCount'); 38 | story.setValue(!pointGiven, 'pointGiven'); 39 | }, 40 | }); 41 | } 42 | 43 | export default { commit }; 44 | -------------------------------------------------------------------------------- /src/mutations/UpdateUser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { commitMutation, graphql } from 'react-relay'; 8 | 9 | function commit(environment, input, done) { 10 | return commitMutation(environment, { 11 | mutation: graphql` 12 | mutation UpdateUserMutation($input: UpdateUserInput!) { 13 | updateUser(input: $input) { 14 | user { 15 | ...UserSettingsDialog_me 16 | id 17 | } 18 | } 19 | } 20 | `, 21 | 22 | variables: { input }, 23 | 24 | onCompleted({ updateUser }, errors) { 25 | done( 26 | errors ? errors[0].state || { '': [errors[0].message] } : null, 27 | updateUser && updateUser.user, 28 | ); 29 | }, 30 | }); 31 | } 32 | 33 | export default { commit }; 34 | -------------------------------------------------------------------------------- /src/mutations/UpsertStory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { commitMutation, graphql } from 'react-relay'; 8 | 9 | function commit(environment, input, done) { 10 | return commitMutation(environment, { 11 | mutation: graphql` 12 | mutation UpsertStoryMutation($input: UpsertStoryInput!) { 13 | upsertStory(input: $input) { 14 | story { 15 | id 16 | title 17 | text 18 | slug 19 | createdAt 20 | updatedAt 21 | } 22 | } 23 | } 24 | `, 25 | 26 | variables: { input }, 27 | 28 | onCompleted({ upsertStory }, errors) { 29 | done( 30 | errors ? errors[0].state || { '': [errors[0].message] } : null, 31 | upsertStory && upsertStory.story, 32 | ); 33 | }, 34 | }); 35 | } 36 | 37 | export default { commit }; 38 | -------------------------------------------------------------------------------- /src/news/Story.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Button from '@material-ui/core/Button'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import { makeStyles } from '@material-ui/core/styles'; 11 | import { createFragmentContainer, graphql } from 'react-relay'; 12 | 13 | import Link from '../common/Link'; 14 | 15 | const useStyles = makeStyles(theme => ({ 16 | root: { 17 | ...theme.mixins.content, 18 | }, 19 | })); 20 | 21 | function Story(props) { 22 | const { 23 | story: { title, text, isURL }, 24 | } = props; 25 | 26 | const s = useStyles(); 27 | 28 | return ( 29 |
30 | 31 | {title} 32 | 33 | {isURL ? ( 34 | 35 | {text} 36 | 37 | ) : ( 38 | text && 39 | text.split('\n').map((x, i) => ( 40 | 41 | {x} 42 | 43 | )) 44 | )} 45 |
46 | 49 |
50 |
51 | ); 52 | } 53 | 54 | export default createFragmentContainer(Story, { 55 | story: graphql` 56 | fragment Story_story on Story { 57 | title 58 | text 59 | isURL 60 | } 61 | `, 62 | }); 63 | -------------------------------------------------------------------------------- /src/news/SubmitDialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Dialog from '@material-ui/core/Dialog'; 9 | import DialogTitle from '@material-ui/core/DialogTitle'; 10 | import DialogContent from '@material-ui/core/DialogContent'; 11 | import DialogContentText from '@material-ui/core/DialogContentText'; 12 | import DialogActions from '@material-ui/core/DialogActions'; 13 | import Button from '@material-ui/core/Button'; 14 | import FormHelperText from '@material-ui/core/FormHelperText'; 15 | import { createFragmentContainer, graphql } from 'react-relay'; 16 | 17 | import TextField from '../common/TextField'; 18 | import UpsertStoryMutation from '../mutations/UpsertStory'; 19 | import { useHistory, useAuth } from '../hooks'; 20 | 21 | const initialState = { 22 | title: '', 23 | text: '', 24 | loading: false, 25 | errors: null, 26 | }; 27 | 28 | function SubmitDialog(props) { 29 | const { me, relay } = props; 30 | const [state, setState] = React.useState({ ...initialState }); 31 | const history = useHistory(); 32 | const auth = useAuth(); 33 | 34 | function handleSubmit(event) { 35 | event.preventDefault(); 36 | setState(x => ({ ...x, loading: true, errors: null })); 37 | UpsertStoryMutation.commit( 38 | relay.environment, 39 | { 40 | title: state.title || '', 41 | text: state.text || '', 42 | }, 43 | (errors, story) => { 44 | if (errors) { 45 | setState(x => ({ ...x, loading: false, errors })); 46 | } else { 47 | props.onClose(); 48 | history.push(`/news/${story.slug}`); 49 | } 50 | }, 51 | ); 52 | } 53 | 54 | function signIn(event) { 55 | event.preventDefault(); 56 | auth.signIn(); 57 | } 58 | 59 | return ( 60 | 61 | Submit a New Story 62 | 63 | 64 | Do you have something cool to share? 65 | 66 |
67 | 74 | 81 | {!me && ( 82 | 83 | Before posting a story you need to{' '} 84 | 85 | sign in 86 | 87 | . 88 | 89 | )} 90 | 91 |
92 | 93 | 94 | 97 | 98 |
99 | ); 100 | } 101 | 102 | export default createFragmentContainer(SubmitDialog, { 103 | me: graphql` 104 | fragment SubmitDialog_me on User { 105 | id 106 | } 107 | `, 108 | }); 109 | -------------------------------------------------------------------------------- /src/news/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { graphql } from 'relay-runtime'; 9 | import Layout from '../common/Layout'; 10 | 11 | export default [ 12 | { 13 | path: '/news', 14 | components: () => [import(/* webpackChunkName: 'news' */ './News')], 15 | query: graphql` 16 | query newsQuery { 17 | ...Layout_data 18 | ...News_data 19 | } 20 | `, 21 | render: ([News], data, { config }) => ({ 22 | title: `News • ${config.app.name}`, 23 | component: ( 24 | 25 | 26 | 27 | ), 28 | chunks: ['news'], 29 | }), 30 | }, 31 | { 32 | path: '/news/:slug', 33 | components: () => [import(/* webpackChunkName: 'story' */ './Story')], 34 | query: graphql` 35 | query newsStoryQuery($slug: String!) { 36 | ...Layout_data 37 | story(slug: $slug) { 38 | ...Story_story 39 | title 40 | slug 41 | } 42 | } 43 | `, 44 | render: ([Story], data, ctx) => { 45 | if (data.story && data.story.slug !== ctx.params.slug) { 46 | return { status: 301, redirect: `/news/${data.story.slug}` }; 47 | } else if (data.story) { 48 | return { 49 | title: data.story.title, 50 | component: ( 51 | 52 | 53 | 54 | ), 55 | chunks: ['story'], 56 | }; 57 | } 58 | }, 59 | }, 60 | ]; 61 | -------------------------------------------------------------------------------- /src/relay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { Environment, Network, RecordSource, Store } from 'relay-runtime'; 8 | import loading from './utils/loading'; 9 | 10 | export function createRelay() { 11 | function fetchQuery(operation, variables, cacheConfig = {}) { 12 | // Instead of making an actual HTTP request to the API, use 13 | // hydrated data available during the initial page load. 14 | if (window.data !== undefined) { 15 | cacheConfig.payload = window.data; 16 | delete window.data; 17 | } 18 | 19 | if (cacheConfig.payload) { 20 | return Promise.resolve(cacheConfig.payload); 21 | } 22 | 23 | loading.notifyStart(); 24 | 25 | return fetch('/graphql', { 26 | method: 'POST', 27 | headers: { 28 | 'content-type': 'application/json', 29 | }, 30 | body: JSON.stringify({ 31 | query: operation.text, 32 | variables, 33 | }), 34 | credentials: 'include', 35 | }) 36 | .then(res => res.json()) 37 | .then(payload => { 38 | // Passes the raw payload up to the caller (see src/router.js). 39 | // This is needed in order to optimize the initial rendering. 40 | cacheConfig.payload = payload; 41 | return payload; 42 | }) 43 | .finally(() => { 44 | loading.notifyStop(); 45 | }); 46 | } 47 | 48 | const recordSource = new RecordSource(); 49 | const store = new Store(recordSource); 50 | const network = Network.create(fetchQuery); 51 | 52 | return new Environment({ store, network }); 53 | } 54 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import UniversalRouter from 'universal-router'; 8 | import { fetchQuery } from 'relay-runtime'; 9 | 10 | import landing from './landing'; 11 | import legal from './legal'; 12 | import misc from './misc'; 13 | import user from './user'; 14 | import news from './news'; 15 | 16 | const routes = [ 17 | ...landing, 18 | ...legal, 19 | ...misc, 20 | ...user, 21 | ...news, 22 | { 23 | path: '/admin', 24 | children: () => import(/* webpackChunkName: 'admin' */ './admin'), 25 | }, 26 | ]; 27 | 28 | function resolveRoute(ctx) { 29 | const { route, params, relay } = ctx; 30 | 31 | // Allow to load routes on demand 32 | if (typeof route.children === 'function') { 33 | return route.children().then(x => { 34 | route.children = x.default; 35 | return undefined; 36 | }); 37 | } 38 | 39 | // Skip routes without render() function 40 | if (!route.render) { 41 | return undefined; 42 | } 43 | 44 | // Start fetching data from GraphQL API 45 | const cacheConfig = { payload: null }; 46 | const variables = route.variables ? route.variables(params, ctx) : params; 47 | const dataPromise = 48 | route.query && fetchQuery(relay, route.query, variables, cacheConfig); 49 | 50 | // Start downloading missing JavaScript chunks 51 | const componentsPromise = route.components 52 | ? route.components().map(x => x.then(x => x.default)) 53 | : []; 54 | 55 | return Promise.all([...componentsPromise, dataPromise]).then(components => { 56 | // GraphQL API response 57 | const data = components.pop(); 58 | const { payload } = cacheConfig; 59 | 60 | // If API response contains an authentication error, 61 | // redirect the user to a login page 62 | const error = ((payload && payload.errors) || []) 63 | .map(x => x.originalError || x) 64 | .find(x => [401, 403].includes(x.code)); 65 | 66 | if (error) { 67 | const errorMsg = encodeURIComponent(error.message); 68 | const returnTo = encodeURIComponent(ctx.pathname); 69 | return { 70 | redirect: `/login?error=${errorMsg}&return=${returnTo}`, 71 | }; 72 | } 73 | 74 | const renderContext = { ...ctx, variables }; 75 | const result = route.render(components, data, renderContext); 76 | return result 77 | ? { 78 | ...result, 79 | query: route.query, 80 | variables, 81 | data, 82 | payload, 83 | render: props => 84 | route.render(components, props, renderContext).component, 85 | } 86 | : undefined; 87 | }); 88 | } 89 | 90 | function errorHandler(error) { 91 | return { 92 | title: error.status === 404 ? 'Page not found' : 'System Error', 93 | status: error.status || 500, 94 | error, 95 | }; 96 | } 97 | 98 | export default new UniversalRouter(routes, { 99 | resolveRoute, 100 | errorHandler, 101 | }); 102 | -------------------------------------------------------------------------------- /src/server/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | import expressGraphQL from 'express-graphql'; 10 | import { Router } from 'express'; 11 | import { printSchema } from 'graphql'; 12 | 13 | import passport from './passport'; 14 | import schema from './schema'; 15 | import templates from './templates'; 16 | import { Context } from './context'; 17 | 18 | const router = new Router(); 19 | 20 | router.use(passport.initialize()); 21 | router.use(passport.session()); 22 | 23 | if (process.env.NODE_ENV !== 'production') { 24 | fs.writeFileSync( 25 | path.join(process.cwd(), 'schema.graphql'), 26 | printSchema(schema, { commentDescriptions: true }), 27 | 'utf8', 28 | ); 29 | } 30 | 31 | if (process.env.APP_ENV !== 'production') { 32 | router.get('/graphql/model', (req, res) => { 33 | res.send(templates.dataModel()); 34 | }); 35 | } 36 | 37 | router.use( 38 | '/graphql', 39 | expressGraphQL(req => ({ 40 | schema, 41 | context: new Context(req), 42 | graphiql: process.env.APP_ENV !== 'production', 43 | pretty: false, 44 | customFormatErrorFn: err => { 45 | console.error(err.originalError || err); 46 | return { 47 | message: err.message, 48 | code: err.originalError && err.originalError.code, 49 | state: err.originalError && err.originalError.state, 50 | locations: err.locations, 51 | path: err.path, 52 | }; 53 | }, 54 | })), 55 | ); 56 | 57 | export default router; 58 | -------------------------------------------------------------------------------- /src/server/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { Router } from 'express'; 8 | 9 | import passport from './passport'; 10 | import login from './login'; 11 | import api from './api'; 12 | import ssr from './ssr'; 13 | 14 | const router = new Router(); 15 | 16 | router.use(passport.initialize()); 17 | router.use(passport.session()); 18 | 19 | router.use(login); 20 | router.use(api); 21 | router.use(ssr); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /src/server/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Application settings to be used in the React app that will be 3 | * available on the context. For example: 4 | * 5 | * import { useConfig } from '../hooks'; 6 | * 7 | * function Title() { 8 | * const { app } = useConfig(); 9 | * return ({app.name}); 10 | * } 11 | * 12 | * IMPORTANT NOTE: Do not include any sensitive data into this file! 13 | */ 14 | export default { 15 | // Core application settings 16 | app: { 17 | name: process.env.APP_NAME, 18 | description: process.env.APP_DESCRIPTION, 19 | origin: process.env.APP_ORIGIN, 20 | version: process.env.APP_VERSION, 21 | env: process.env.APP_ENV, 22 | }, 23 | 24 | // Firebase 25 | // https://firebase.google.com/docs/web/setup 26 | firebase: { 27 | projectId: process.env.GCP_PROJECT, 28 | authDomain: process.env.APP_ORIGIN.startsWith('http://localhost') 29 | ? `${process.env.GCP_PROJECT}.firebaseapp.com` 30 | : process.env.APP_ORIGIN.replace(/^https?:\/\//, ''), 31 | apiKey: process.env.GCP_BROWSER_KEY, 32 | }, 33 | 34 | // Facebook SDK for JavaScript (src/utils/fb.js) 35 | // https://developers.facebook.com/docs/javascript/quickstart 36 | facebook: { 37 | appId: process.env.FACEBOOK_APP_ID, 38 | pageId: process.env.FACEBOOK_PAGE_ID, 39 | }, 40 | 41 | // Analytics 42 | gaTrackingId: process.env.GA_TRACKING_ID, 43 | 44 | // Google Cloud Platform API Key 45 | gcpServiceKey: process.env.GCP_BROWSER_KEY, 46 | }; 47 | -------------------------------------------------------------------------------- /src/server/context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import DataLoader from 'dataloader'; 8 | 9 | import db from './db'; 10 | import { Validator } from './validator'; 11 | import { mapTo, mapToMany, mapToValues } from './utils'; 12 | import { UnauthorizedError, ForbiddenError, ValidationError } from './errors'; 13 | 14 | export class Context { 15 | errors = []; 16 | 17 | constructor(req) { 18 | if (req.user) { 19 | // Add user object to the cache 20 | this.userById.prime(req.user.id, req.user); 21 | this.userByUsername.prime(req.user.username, req.user); 22 | 23 | // Convert snake_case fields to camelCase for convinience 24 | Object.keys(req.user).forEach(key => { 25 | req.user[key.replace(/_\w/g, x => x[1].toUpperCase())] = req.user[key]; 26 | }); 27 | 28 | this.user = req.user; 29 | } else { 30 | this.user = null; 31 | } 32 | 33 | // Some GraphQL mutations may need to sign in / sign out a user 34 | this.logIn = req.logIn; 35 | this.logOut = req.logOut; 36 | this.ip = req.ip; 37 | } 38 | 39 | /* 40 | * Authorization 41 | * ------------------------------------------------------------------------ */ 42 | 43 | ensureIsAuthorized(check) { 44 | if (!this.user) { 45 | throw new UnauthorizedError(); 46 | } 47 | 48 | if (check && !check(this.user)) { 49 | throw new ForbiddenError(); 50 | } 51 | } 52 | 53 | /* 54 | * Validation 55 | * ------------------------------------------------------------------------ */ 56 | 57 | addError(key, message) { 58 | this.errors.push({ key, message }); 59 | } 60 | 61 | validate(input, mode) { 62 | const validator = new Validator(input, mode, errors => { 63 | throw new ValidationError(errors); 64 | }); 65 | 66 | return transform => { 67 | transform(validator); 68 | return validator.validate(); 69 | }; 70 | } 71 | 72 | /* 73 | * Data loaders 74 | * ------------------------------------------------------------------------ */ 75 | 76 | userById = new DataLoader(keys => 77 | db 78 | .table('users') 79 | .whereIn('id', keys) 80 | .select() 81 | .then(rows => 82 | rows.map(x => { 83 | this.userByUsername.prime(x.username, x); 84 | return x; 85 | }), 86 | ) 87 | .then(mapTo(keys, x => x.id)), 88 | ); 89 | 90 | userByUsername = new DataLoader(keys => 91 | db 92 | .table('users') 93 | .whereIn('username', keys) 94 | .select() 95 | .then(rows => 96 | rows.map(x => { 97 | this.userById.prime(x.id, x); 98 | return x; 99 | }), 100 | ) 101 | .then(mapTo(keys, x => x.username)), 102 | ); 103 | 104 | identitiesByUserId = new DataLoader(keys => 105 | db 106 | .table('user_identities') 107 | .whereIn('user_id', keys) 108 | .select() 109 | .then(mapToMany(keys, x => x.user_id)), 110 | ); 111 | 112 | storyById = new DataLoader(keys => 113 | db 114 | .table('stories') 115 | .whereIn('id', keys) 116 | .select() 117 | .then(rows => { 118 | rows.forEach(x => this.storyBySlug.prime(x.slug, x)); 119 | return rows; 120 | }) 121 | .then(mapTo(keys, x => x.id)), 122 | ); 123 | 124 | storyBySlug = new DataLoader(keys => 125 | db 126 | .table('stories') 127 | .whereIn('slug', keys) 128 | .select() 129 | .then(rows => { 130 | rows.forEach(x => this.storyById.prime(x.id, x)); 131 | return rows; 132 | }) 133 | .then(mapTo(keys, x => x.slug)), 134 | ); 135 | 136 | storyPointsCount = new DataLoader(keys => 137 | db 138 | .table('stories') 139 | .leftJoin('story_points', 'story_points.story_id', 'stories.id') 140 | .whereIn('stories.id', keys) 141 | .groupBy('stories.id') 142 | .select('stories.id', db.raw('count(story_points.user_id)::int')) 143 | .then( 144 | mapToValues( 145 | keys, 146 | x => x.id, 147 | x => parseInt(x.count, 10), 148 | ), 149 | ), 150 | ); 151 | 152 | storyPointGiven = new DataLoader(keys => { 153 | const { id: userId } = this.user; 154 | 155 | return db 156 | .table('stories') 157 | .leftJoin('story_points', function join() { 158 | this.on('story_points.story_id', 'stories.id').andOn( 159 | 'story_points.user_id', 160 | db.raw('?', [userId]), 161 | ); 162 | }) 163 | .whereIn('stories.id', keys) 164 | .select( 165 | 'stories.id', 166 | db.raw('(story_points.user_id IS NOT NULL) AS given'), 167 | ) 168 | .then( 169 | mapToValues( 170 | keys, 171 | x => x.id, 172 | x => x.given, 173 | ), 174 | ); 175 | }); 176 | } 177 | -------------------------------------------------------------------------------- /src/server/db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import fs from 'fs'; 8 | import knex from 'knex'; 9 | 10 | // Make it easier to identify open database connections by running: 11 | // SELECT * from pg_stat_activity; 12 | if (process.env.X_GOOGLE_GCLOUD_PROJECT) { 13 | process.env.PGAPPNAME = [ 14 | process.env.X_GOOGLE_GCLOUD_PROJECT, 15 | process.env.X_GOOGLE_FUNCTION_NAME, 16 | process.env.X_GOOGLE_FUNCTION_VERSION, 17 | ].join('/'); 18 | } 19 | 20 | const db = knex({ 21 | client: 'pg', 22 | connection: { 23 | min: process.env.X_GOOGLE_FUNCTION_NAME === 'app' ? 1 : 0, 24 | // Database connection pool must be set to max 1 25 | // when running in serverless environment. 26 | max: 1, 27 | // https://github.com/tgriesser/knex/issues/852 28 | ssl: (process.env.PGSSLMODE || 'disable') !== 'disable' && { 29 | rejectUnauthorized: false, 30 | cert: fs.readFileSync(process.env.PGSSLCERT, 'utf8'), 31 | key: fs.readFileSync(process.env.PGSSLKEY, 'utf8'), 32 | ca: fs.readFileSync(process.env.PGSSLROOTCERT, 'utf8'), 33 | }, 34 | }, 35 | }); 36 | 37 | export default db; 38 | -------------------------------------------------------------------------------- /src/server/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | export class ValidationError extends Error { 8 | code = 400; 9 | state; 10 | 11 | constructor(errors) { 12 | super('The request is invalid.'); 13 | this.state = errors.reduce((result, error) => { 14 | if (Object.prototype.hasOwnProperty.call(result, error.key)) { 15 | result[error.key].push(error.message); 16 | } else { 17 | Object.defineProperty(result, error.key, { 18 | value: [error.message], 19 | enumerable: true, 20 | }); 21 | } 22 | return result; 23 | }, {}); 24 | } 25 | } 26 | 27 | export class UnauthorizedError extends Error { 28 | code = 401; 29 | message = this.message || 'Anonymous access is denied.'; 30 | } 31 | 32 | export class ForbiddenError extends Error { 33 | code = 403; 34 | message = this.message || 'Access is denied.'; 35 | } 36 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | const fs = require('fs'); 8 | const dotenv = require('dotenv'); 9 | const express = require('express'); 10 | const firebase = require('firebase-admin'); 11 | const functions = require('firebase-functions'); 12 | 13 | if (process.env.NODE_ENV === 'production') { 14 | process.env.APP_VERSION = fs.readFileSync('./VERSION', 'utf8').trim(); 15 | 16 | // Load API keys, secrets etc. from Firebase environment 17 | // https://firebase.google.com/docs/functions/config-env 18 | const { app: config } = functions.config(); 19 | Object.keys(config).forEach(key => { 20 | process.env[key.toUpperCase()] = 21 | typeof config[key] === 'object' 22 | ? JSON.stringify(config[key]) 23 | : config[key]; 24 | }); 25 | } 26 | 27 | dotenv.config({ path: `.env.${process.env.NODE_ENV}` }); 28 | dotenv.config({ path: '.env.local' }); 29 | dotenv.config({ path: '.env' }); 30 | 31 | // Configure Firebase Admin SDK 32 | // https://firebase.google.com/docs/admin/setup 33 | if (!firebase.apps.length) { 34 | firebase.initializeApp({ 35 | credential: firebase.credential.cert( 36 | JSON.parse(process.env.GCP_SERVICE_KEY), 37 | ), 38 | }); 39 | } 40 | 41 | if (process.env.NODE_ENV === 'production') { 42 | // Server environment 43 | exports.app = functions 44 | .runWith({ memory: '2GB' }) 45 | .https.onRequest(require('./app').default); 46 | } else { 47 | // Local/dev environment 48 | const app = express(); 49 | const db = require('./db').default; 50 | app.use(require('./app').default); 51 | module.exports.default = app; 52 | module.exports.dispose = () => db.destroy(); 53 | } 54 | -------------------------------------------------------------------------------- /src/server/login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import Router from 'express'; 8 | import { toGlobalId } from 'graphql-relay'; 9 | import passport from './passport'; 10 | 11 | const router = new Router(); 12 | 13 | function authenticate(provider) { 14 | return (req, res, next) => { 15 | function send(err, user) { 16 | const data = { 17 | type: 'LOGIN', 18 | error: err ? err.message : undefined, 19 | user: user 20 | ? { 21 | id: toGlobalId('User', user.id), 22 | username: user.username, 23 | email: user.email, 24 | emailVerified: user.email_verified, 25 | displayName: user.display_name, 26 | photoURL: user.photo_url, 27 | timeZone: user.time_zone, 28 | createdAt: user.created_at, 29 | updatedAt: user.updated_at, 30 | lastLoginAt: user.last_login_at, 31 | } 32 | : null, 33 | }; 34 | 35 | res.send(` 36 | `); // prettier-ignore 45 | } 46 | 47 | passport.authenticate(provider, (err, user) => { 48 | if (err) { 49 | send(err); 50 | } else if (user) { 51 | req 52 | .logIn(user) 53 | .then(() => { 54 | send(null, user); 55 | }) 56 | .catch(err => { 57 | send(err); 58 | }); 59 | } else { 60 | send(null, null); 61 | } 62 | })(req, res, next); 63 | }; 64 | } 65 | 66 | router.get( 67 | '/login/google', 68 | passport.authenticate('google', { scope: ['profile', 'email'] }), 69 | ); 70 | 71 | router.get( 72 | '/login/facebook', 73 | passport.authenticate('facebook', { 74 | scope: ['public_profile', 'email'], 75 | }), 76 | ); 77 | 78 | router.get('/login/google/return', authenticate('google')); 79 | router.get('/login/facebook/return', authenticate('facebook')); 80 | 81 | router.post('/login/clear', (req, res) => { 82 | req.logOut(); 83 | res.sendStatus(200); 84 | }); 85 | 86 | export default router; 87 | -------------------------------------------------------------------------------- /src/server/mutations/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Mutations 2 | 3 | Please, prefer creating UPSERT mutations in instead of CREATE + UPDATE whenever possible, this reduces unnecessary code duplication. For example: 4 | 5 | ```js 6 | import { mutationWithClientMutationId } from 'graphql-relay'; 7 | import { GraphQLID, GraphQLString } from 'graphql'; 8 | 9 | import db from '../db'; 10 | import { StoryType } from '../types'; 11 | import { fromGlobalId } from '../utils'; 12 | 13 | export const upsertStory = mutationWithClientMutationId({ 14 | name: 'UpsertStory', 15 | description: 'Creates or updates a story.', 16 | 17 | inputFields: { 18 | id: { type: GraphQLID }, 19 | text: { type: GraphQLString }, 20 | }, 21 | 22 | outputFields: { 23 | story: { type: StoryType }, 24 | }, 25 | 26 | async mutateAndGetPayload({ id, ...data }, ctx) { 27 | ctx.ensureIsAuthorized(); 28 | 29 | let story; 30 | 31 | if (id) { 32 | // Updates an existing story by ID 33 | [story] = await db 34 | .table('stories') 35 | .where({ id: fromGlobalId(id, 'Story') }) 36 | .update(data) 37 | .returning('*'); 38 | } else { 39 | // Otherwise, creates a new story 40 | [story] = await db 41 | .table('stories') 42 | .insert(data) 43 | .returning('*'); 44 | } 45 | 46 | return { story }; 47 | }, 48 | }); 49 | ``` 50 | 51 | Don't forget to check permissions using `ctx.ensureIsAuthorized()` helper method 52 | from `src/server/context.js`. For example: 53 | 54 | ```js 55 | const story = await db 56 | .table('stories') 57 | .where({ id }) 58 | .first(); 59 | 60 | ctx.ensureIsAuthorized(user => story.author_id === user.id); 61 | ``` 62 | 63 | Always validate user and sanitize user input! We use [`validator.js`](https://github.com/validatorjs/validator.js) + a custom helper function `ctx.validate(input)(...)` for that. For example: 64 | 65 | ```js 66 | const data = await ctx.validate(input, id ? 'update' : 'create')(x => 67 | x 68 | .field('title', { trim: true }) 69 | .isRequired() 70 | .isLength({ min: 5, max: 80 }) 71 | 72 | .field('text', { alias: 'URL or text', trim: true }) 73 | .isRequired() 74 | .isLength({ min: 10, max: 1000 }), 75 | ); 76 | 77 | await db 78 | .table('stories') 79 | .where({ id }) 80 | .update(data); 81 | ``` 82 | -------------------------------------------------------------------------------- /src/server/mutations/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | export * from './user'; 8 | export * from './story'; 9 | -------------------------------------------------------------------------------- /src/server/mutations/story.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import uuid from 'uuid'; 8 | import slugify from 'slugify'; 9 | import validator from 'validator'; 10 | import { mutationWithClientMutationId } from 'graphql-relay'; 11 | import { 12 | GraphQLNonNull, 13 | GraphQLID, 14 | GraphQLString, 15 | GraphQLBoolean, 16 | } from 'graphql'; 17 | 18 | import db from '../db'; 19 | import { StoryType } from '../types'; 20 | import { fromGlobalId } from '../utils'; 21 | 22 | function slug(text) { 23 | return slugify(text, { lower: true }); 24 | } 25 | 26 | export const upsertStory = mutationWithClientMutationId({ 27 | name: 'UpsertStory', 28 | description: 'Creates or updates a story.', 29 | 30 | inputFields: { 31 | id: { type: GraphQLID }, 32 | title: { type: GraphQLString }, 33 | text: { type: GraphQLString }, 34 | approved: { type: GraphQLBoolean }, 35 | validateOnly: { type: GraphQLBoolean }, 36 | }, 37 | 38 | outputFields: { 39 | story: { type: StoryType }, 40 | }, 41 | 42 | async mutateAndGetPayload(input, ctx) { 43 | const id = input.id ? fromGlobalId(input.id, 'Story') : null; 44 | const newId = uuid.v4(); 45 | 46 | let story; 47 | 48 | if (id) { 49 | story = await db 50 | .table('stories') 51 | .where({ id }) 52 | .first(); 53 | 54 | if (!story) { 55 | throw new Error(`Cannot find the story # ${id}.`); 56 | } 57 | 58 | // Only the author of the story or admins can edit it 59 | ctx.ensureIsAuthorized( 60 | user => story.author_id === user.id || user.isAdmin, 61 | ); 62 | } else { 63 | ctx.ensureIsAuthorized(); 64 | } 65 | 66 | // Validate and sanitize user input 67 | const data = await ctx.validate( 68 | input, 69 | id ? 'update' : 'create', 70 | )(x => 71 | x 72 | .field('title', { trim: true }) 73 | .isRequired() 74 | .isLength({ min: 5, max: 80 }) 75 | 76 | .field('text', { alias: 'URL or text', trim: true }) 77 | .isRequired() 78 | .isLength({ min: 10, max: 1000 }) 79 | 80 | .field('text', { 81 | trim: true, 82 | as: 'is_url', 83 | transform: x => validator.isURL(x, { protocols: ['http', 'https'] }), 84 | }) 85 | 86 | .field('approved') 87 | .is(() => ctx.user.isAdmin, 'Only admins can approve a story.'), 88 | ); 89 | 90 | if (data.title) { 91 | data.slug = `${slug(data.title)}-${(id || newId).substr(29)}`; 92 | } 93 | 94 | if (id && Object.keys(data).length) { 95 | [story] = await db 96 | .table('stories') 97 | .where({ id }) 98 | .update({ ...data, updated_at: db.fn.now() }) 99 | .returning('*'); 100 | } else { 101 | [story] = await db 102 | .table('stories') 103 | .insert({ 104 | id: newId, 105 | ...data, 106 | author_id: ctx.user.id, 107 | approved: ctx.user.isAdmin ? true : false, 108 | }) 109 | .returning('*'); 110 | } 111 | 112 | return { story }; 113 | }, 114 | }); 115 | 116 | export const likeStory = mutationWithClientMutationId({ 117 | name: 'LikeStory', 118 | description: 'Marks the story as "liked".', 119 | 120 | inputFields: { 121 | id: { type: new GraphQLNonNull(GraphQLID) }, 122 | }, 123 | 124 | outputFields: { 125 | story: { type: StoryType }, 126 | }, 127 | 128 | async mutateAndGetPayload(input, ctx) { 129 | // Check permissions 130 | ctx.ensureIsAuthorized(); 131 | 132 | const id = fromGlobalId(input.id, 'Story'); 133 | const keys = { story_id: id, user_id: ctx.user.id }; 134 | 135 | const points = await db 136 | .table('story_points') 137 | .where(keys) 138 | .select(1); 139 | 140 | if (points.length) { 141 | await db 142 | .table('story_points') 143 | .where(keys) 144 | .del(); 145 | } else { 146 | await db.table('story_points').insert(keys); 147 | } 148 | 149 | const story = db 150 | .table('stories') 151 | .where({ id }) 152 | .first(); 153 | 154 | return { story }; 155 | }, 156 | }); 157 | -------------------------------------------------------------------------------- /src/server/mutations/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { mutationWithClientMutationId } from 'graphql-relay'; 8 | import { 9 | GraphQLNonNull, 10 | GraphQLID, 11 | GraphQLString, 12 | GraphQLBoolean, 13 | } from 'graphql'; 14 | 15 | import db from '../db'; 16 | import { UserType } from '../types'; 17 | import { fromGlobalId } from '../utils'; 18 | 19 | export const updateUser = mutationWithClientMutationId({ 20 | name: 'UpdateUser', 21 | description: 'Updates a user.', 22 | 23 | inputFields: { 24 | id: { type: new GraphQLNonNull(GraphQLID) }, 25 | username: { type: GraphQLString }, 26 | email: { type: GraphQLString }, 27 | displayName: { type: GraphQLString }, 28 | photoURL: { type: GraphQLString }, 29 | timeZone: { type: GraphQLString }, 30 | isAdmin: { type: GraphQLBoolean }, 31 | validateOnly: { type: GraphQLBoolean }, 32 | }, 33 | 34 | outputFields: { 35 | user: { type: UserType }, 36 | }, 37 | 38 | async mutateAndGetPayload(input, ctx) { 39 | const id = fromGlobalId(input.id, 'User'); 40 | 41 | // Check permissions 42 | ctx.ensureIsAuthorized(user => user.id === id || user.isAdmin); 43 | 44 | function usernameAvailable(username) { 45 | return db 46 | .table('users') 47 | .where({ username }) 48 | .whereNot({ id }) 49 | .select(1) 50 | .then(x => !x.length); 51 | } 52 | 53 | // Validate and sanitize user input 54 | const data = await ctx.validate( 55 | input, 56 | 'update', 57 | )(x => 58 | x 59 | .field('username', { trim: true }) 60 | .isLength({ min: 1, max: 50 }) 61 | .is(usernameAvailable, 'That username is taken. Try another.') 62 | 63 | .field('email') 64 | .isLength({ max: 100 }) 65 | .isEmail() 66 | 67 | .field('displayName', { as: 'display_name', trim: true }) 68 | .isLength({ min: 1, max: 100 }) 69 | 70 | .field('photoURL', { as: 'photo_url' }) 71 | .isLength({ max: 250 }) 72 | .isURL() 73 | 74 | .field('timeZone', { as: 'time_zone' }) 75 | .isLength({ max: 50 }) 76 | 77 | .field('isAdmin', { as: 'is_admin' }) 78 | .is(() => ctx.user.isAdmin, 'Only admins can change this field.'), 79 | ); 80 | 81 | if (input.validateOnly) { 82 | return { user: null }; 83 | } 84 | 85 | let user; 86 | 87 | if (Object.keys(data).length) { 88 | [user] = await db 89 | .table('users') 90 | .where({ id }) 91 | .update({ ...data, updated_at: db.fn.now() }) 92 | .returning('*'); 93 | } 94 | 95 | return { user }; 96 | }, 97 | }); 98 | 99 | export const deleteUser = mutationWithClientMutationId({ 100 | name: 'DeleteUser', 101 | description: 'Deletes a user.', 102 | 103 | inputFields: { 104 | id: { type: new GraphQLNonNull(GraphQLID) }, 105 | }, 106 | 107 | outputFields: { 108 | deletedUserId: { 109 | type: GraphQLString, 110 | }, 111 | }, 112 | 113 | async mutateAndGetPayload(input, ctx) { 114 | // Check permissions 115 | ctx.ensureIsAuthorized(user => user.isAdmin); 116 | 117 | const id = fromGlobalId(input.id, 'User'); 118 | 119 | await db 120 | .table('users') 121 | .where({ id }) 122 | .del(); 123 | 124 | return { deletedUserId: input.id }; 125 | }, 126 | }); 127 | -------------------------------------------------------------------------------- /src/server/node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | /* eslint-disable global-require */ 8 | 9 | import { nodeDefinitions, fromGlobalId } from 'graphql-relay'; 10 | import { assignType, getType } from './utils'; 11 | 12 | export const { nodeInterface, nodeField, nodesField } = nodeDefinitions( 13 | (globalId, context) => { 14 | const { type, id } = fromGlobalId(globalId); 15 | 16 | switch (type) { 17 | case 'User': 18 | return context.userById.load(id).then(assignType('User')); 19 | case 'Story': 20 | return context.storyById.load(id).then(assignType('Story')); 21 | case 'Comment': 22 | return context.commentById.load(id).then(assignType('Comment')); 23 | default: 24 | return null; 25 | } 26 | }, 27 | obj => { 28 | switch (getType(obj)) { 29 | case 'User': 30 | return require('./types').UserType; 31 | case 'Story': 32 | return require('./types').StoryType; 33 | case 'Comment': 34 | return require('./types').CommentType; 35 | default: 36 | return null; 37 | } 38 | }, 39 | ); 40 | -------------------------------------------------------------------------------- /src/server/passport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import uuid from 'uuid'; 8 | import passport from 'passport'; 9 | import jwt from 'jwt-passport'; 10 | import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; 11 | import { Strategy as FacebookStrategy } from 'passport-facebook'; 12 | 13 | import db from './db'; 14 | import { upsertUser } from './utils'; 15 | 16 | const origin = 17 | process.env.NODE_ENV === 'production' ? `${process.env.APP_ORIGIN}` : ''; 18 | 19 | passport.framework( 20 | jwt({ 21 | name: process.env.JWT_NAME, 22 | secret: process.env.JWT_SECRET, 23 | issuer: origin, 24 | expiresIn: '1y', 25 | cookie: { 26 | maxAge: 31536000000 /* 1 year */, 27 | }, 28 | createToken: req => ({ 29 | sub: req.user.id, 30 | jti: uuid.v4(), 31 | }), 32 | saveToken: token => 33 | db.table('user_tokens').insert({ 34 | user_id: token.sub, 35 | token_id: token.jti, 36 | }), 37 | deleteToken: token => 38 | db 39 | .table('user_tokens') 40 | .where({ token_id: token.jti }) 41 | .del(), 42 | findUser: token => 43 | db 44 | .table('user_tokens') 45 | .leftJoin('users', 'users.id', 'user_tokens.user_id') 46 | .where({ 'user_tokens.token_id': token.jti }) 47 | .select('users.*') 48 | .first(), 49 | }), 50 | ); 51 | 52 | // https://github.com/jaredhanson/passport-google-oauth2 53 | passport.use( 54 | new GoogleStrategy( 55 | { 56 | clientID: process.env.GOOGLE_CLIENT_ID, 57 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 58 | callbackURL: `${origin}/login/google/return`, 59 | passReqToCallback: true, 60 | }, 61 | (req, accessToken, refreshToken, profile, cb) => { 62 | const credentials = { accessToken, refreshToken }; 63 | upsertUser(profile, credentials) 64 | .then(user => cb(null, user)) 65 | .catch(err => cb(err)); 66 | }, 67 | ), 68 | ); 69 | 70 | // https://github.com/jaredhanson/passport-facebook 71 | passport.use( 72 | new FacebookStrategy( 73 | { 74 | clientID: process.env.FACEBOOK_APP_ID, 75 | clientSecret: process.env.FACEBOOK_APP_SECRET, 76 | callbackURL: `${origin}/login/facebook/return`, 77 | profileFields: [ 78 | 'id', 79 | 'cover', 80 | 'name', 81 | 'displayName', 82 | 'age_range', 83 | 'link', 84 | 'gender', 85 | 'locale', 86 | 'picture', 87 | 'timezone', 88 | 'updated_time', 89 | 'verified', 90 | 'email', 91 | ], 92 | passReqToCallback: true, 93 | }, 94 | (req, accessToken, refreshToken, profile, cb) => { 95 | const credentials = { accessToken, refreshToken }; 96 | upsertUser(profile, credentials) 97 | .then(user => cb(null, user)) 98 | .catch(err => cb(err)); 99 | }, 100 | ), 101 | ); 102 | 103 | export default passport; 104 | -------------------------------------------------------------------------------- /src/server/queries/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Query Fields 2 | 3 | Please prefer using the general purpose `node(id)` and `nodes(ids)` query fields 4 | whenever possible. For example, the client app must be able to retrieve most data 5 | entities by their IDs: 6 | 7 | ```graphql 8 | query { 9 | story: node(id: "xxx") { 10 | ... on Story { 11 | id 12 | text 13 | } 14 | } 15 | 16 | stories: nodes(ids: ["xxx", "xxx"]) { 17 | ... on Story { 18 | id 19 | text 20 | } 21 | } 22 | } 23 | ``` 24 | 25 | Only when these two methods are not suffice you would need to declare a custom 26 | query field. For example: 27 | 28 | ```js 29 | import { GraphQLNonNull, GraphQLString } from 'graphql'; 30 | 31 | import db from '../db'; 32 | import { StoryType } from '../types'; 33 | 34 | export default { 35 | type: StoryType, 36 | 37 | args: { 38 | slug: { type: new GraphQLNonNull(GraphQLString) }, 39 | }, 40 | 41 | async resolve(root, { slug }, ctx) { 42 | let story = await db 43 | .table('stories') 44 | .where({ slug }) 45 | .first(); 46 | 47 | return story; 48 | }, 49 | }; 50 | ``` 51 | 52 | Avoid creating more than one/two fields per GraphQL type. For example: 53 | 54 | ```graphql 55 | # BAD 56 | query { 57 | allStories { 58 | ...Story 59 | } 60 | likedStories { 61 | ...Story 62 | } 63 | storiesByAuthorId(id: "xxx") { 64 | ...Story 65 | } 66 | } 67 | 68 | # BETTER 69 | query { 70 | allStories: stories { 71 | ...Story 72 | } 73 | likedStories: stories(liked: true) { 74 | ...Story 75 | } 76 | storiesByAuthor: stories(author: "username") { 77 | ...Story 78 | } 79 | } 80 | ``` 81 | 82 | Implement Relay interface for those fields that need to support pagination. 83 | -------------------------------------------------------------------------------- /src/server/queries/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | export * from './user'; 8 | export * from './story'; 9 | -------------------------------------------------------------------------------- /src/server/queries/story.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { GraphQLList, GraphQLNonNull, GraphQLString } from 'graphql'; 8 | 9 | import db from '../db'; 10 | import { StoryType } from '../types'; 11 | 12 | export const story = { 13 | type: StoryType, 14 | 15 | args: { 16 | slug: { type: new GraphQLNonNull(GraphQLString) }, 17 | }, 18 | 19 | async resolve(root, { slug }, ctx) { 20 | let story = await db 21 | .table('stories') 22 | .where({ slug }) 23 | .first(); 24 | 25 | // Attempts to find a story by partial ID contained in the slug. 26 | if (!story) { 27 | const match = slug.match(/[a-f0-9]{7}$/); 28 | if (match) { 29 | story = await db 30 | .table('stories') 31 | .whereRaw(`id::text LIKE '%${match[0]}'`) 32 | .first(); 33 | } 34 | } 35 | 36 | return story; 37 | }, 38 | }; 39 | 40 | export const stories = { 41 | type: new GraphQLList(StoryType), 42 | 43 | resolve(self, args, ctx) { 44 | return db 45 | .table('stories') 46 | .where({ approved: true }) 47 | .orWhere({ approved: false, author_id: ctx.user ? ctx.user.id : null }) 48 | .orderBy('created_at', 'desc') 49 | .limit(100) 50 | .select(); 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/server/queries/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { GraphQLNonNull, GraphQLString } from 'graphql'; 8 | import { 9 | connectionDefinitions, 10 | forwardConnectionArgs, 11 | connectionFromArraySlice, 12 | cursorToOffset, 13 | } from 'graphql-relay'; 14 | 15 | import db from '../db'; 16 | import { countField } from '../utils'; 17 | import { UserType } from '../types'; 18 | 19 | export const me = { 20 | type: UserType, 21 | 22 | resolve(root, args, ctx) { 23 | return ctx.user ? ctx.userById.load(ctx.user.id) : null; 24 | }, 25 | }; 26 | 27 | export const user = { 28 | type: UserType, 29 | 30 | args: { 31 | username: { type: new GraphQLNonNull(GraphQLString) }, 32 | }, 33 | 34 | resolve(root, { username }, ctx) { 35 | return ctx.userByUsername.load(username); 36 | }, 37 | }; 38 | 39 | export const users = { 40 | type: connectionDefinitions({ 41 | name: 'User', 42 | nodeType: UserType, 43 | connectionFields: { totalCount: countField }, 44 | }).connectionType, 45 | 46 | args: forwardConnectionArgs, 47 | 48 | async resolve(root, args, ctx) { 49 | // Only admins are allowed to fetch the list of users 50 | ctx.ensureIsAuthorized(user => user.isAdmin); 51 | 52 | const query = db.table('users'); 53 | 54 | const limit = args.first === undefined ? 50 : args.first; 55 | const offset = args.after ? cursorToOffset(args.after) + 1 : 0; 56 | 57 | const data = await query 58 | .clone() 59 | .limit(limit) 60 | .offset(offset) 61 | .orderBy('created_at', 'desc') 62 | .select(); 63 | 64 | data.forEach(x => { 65 | ctx.userById.prime(x.id, x); 66 | ctx.userByUsername.prime(x.username, x); 67 | }); 68 | 69 | return { 70 | ...connectionFromArraySlice(data, args, { 71 | sliceStart: offset, 72 | arrayLength: offset + data.length, 73 | }), 74 | query, 75 | }; 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /src/server/relay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { graphql } from 'graphql'; 8 | import { Environment, Network, RecordSource, Store } from 'relay-runtime'; 9 | 10 | import schema from './schema'; 11 | import { Context } from './context'; 12 | 13 | export function createRelay(req) { 14 | function fetchQuery(operation, variables, cacheConfig) { 15 | return graphql({ 16 | schema, 17 | source: operation.text, 18 | contextValue: new Context(req), 19 | variableValues: variables, 20 | operationName: operation.name, 21 | }).then(payload => { 22 | // Passes the raw payload up to the caller (see src/router.js). 23 | // This is needed in order to hydrate/de-hydrate that 24 | // data on the client during the initial page load. 25 | cacheConfig.payload = payload; 26 | return payload; 27 | }); 28 | } 29 | 30 | const recordSource = new RecordSource(); 31 | const store = new Store(recordSource); 32 | const network = Network.create(fetchQuery); 33 | 34 | return new Environment({ store, network }); 35 | } 36 | -------------------------------------------------------------------------------- /src/server/schema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { GraphQLSchema, GraphQLObjectType } from 'graphql'; 8 | 9 | import * as queries from './queries'; 10 | import * as mutations from './mutations'; 11 | import { nodeField, nodesField } from './node'; 12 | 13 | export default new GraphQLSchema({ 14 | query: new GraphQLObjectType({ 15 | name: 'Query', 16 | fields: { 17 | node: nodeField, 18 | nodes: nodesField, 19 | ...queries, 20 | }, 21 | }), 22 | 23 | mutation: new GraphQLObjectType({ 24 | name: 'Mutation', 25 | fields: mutations, 26 | }), 27 | }); 28 | -------------------------------------------------------------------------------- /src/server/ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import qs from 'query-string'; 8 | import serialize from 'serialize-javascript'; 9 | import React from 'react'; 10 | import ReactDOM from 'react-dom/server'; 11 | import { createMemoryHistory } from 'history'; 12 | import { Router } from 'express'; 13 | 14 | import App from '../common/App'; 15 | import config from './config'; 16 | import templates from './templates'; 17 | import routes from '../router'; 18 | import stats from './stats.json'; // eslint-disable-line 19 | import { createRelay } from './relay'; 20 | 21 | const router = new Router(); 22 | 23 | // Static assets are supposed to be served via CDN. 24 | router.get('/static/*', (req, res) => { 25 | res.status(404); 26 | res.type('text/plain'); 27 | res.send('Not found'); 28 | }); 29 | 30 | router.get('*', async (req, res, next) => { 31 | try { 32 | const { path: pathname, originalUrl: url } = req; 33 | const history = createMemoryHistory({ initialEntries: [pathname] }); 34 | const relay = createRelay(req); 35 | 36 | // Prefer using the same query string parser in both 37 | // browser and Node.js environments 38 | const search = url.includes('?') ? url.substr(url.indexOf('?') + 1) : ''; 39 | const query = qs.parse(search); 40 | 41 | // Resolves a route matching the provided URL path (location) 42 | const route = await routes.resolve({ pathname, query, relay, config }); 43 | 44 | if (route.redirect) { 45 | res.redirect(route.status || 302, route.redirect); 46 | return; 47 | } 48 | 49 | let body; 50 | 51 | // Full server-side rendering for some routes like landing pages etc. 52 | if (route.ssr === true) { 53 | try { 54 | body = ReactDOM.renderToString( 55 | , 56 | ); 57 | } catch (err) { 58 | console.error(err); 59 | } 60 | } 61 | 62 | if (route.status && route.status !== 200) { 63 | res.status(route.status); 64 | } 65 | 66 | res.send( 67 | templates.ok({ 68 | url: `${process.env.APP_ORIGIN}${req.path}`, 69 | title: route.title, 70 | description: route.description, 71 | preload: route.preload || [], 72 | assets: (route.chunks || []).reduce( 73 | (acc, name) => [...acc, ...[].concat(stats.assetsByChunkName[name])], 74 | stats.entrypoints.main.assets, 75 | ), 76 | data: serialize(route.payload, { isJSON: true }), 77 | body, 78 | config: JSON.stringify(config), 79 | env: process.env, 80 | }), 81 | ); 82 | } catch (err) { 83 | next(err); 84 | } 85 | }); 86 | 87 | export default router; 88 | -------------------------------------------------------------------------------- /src/server/templates/data-model.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Data Model • React Starter Kit for Firebase 7 | 8 | 9 | 10 | 11 | 12 | 13 | 27 | 28 | 29 |
Loading...
30 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/server/templates/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found 6 | 7 | 49 | 50 | 51 |

Page Not Found

52 |

Sorry, but the page you were trying to view does not exist.

53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/server/templates/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | /* eslint-disable import/no-webpack-loader-syntax */ 8 | 9 | import ejs from 'ejs'; 10 | 11 | import ok from '!!raw-loader!./ok.ejs'; 12 | import error from '!!raw-loader!./error.ejs'; 13 | import dataModel from '!!raw-loader!./data-model.ejs'; 14 | 15 | export default { 16 | ok: ejs.compile(ok), 17 | error: ejs.compile(error), 18 | dataModel: ejs.compile(dataModel), 19 | }; 20 | -------------------------------------------------------------------------------- /src/server/templates/ok.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= title %> 7 | 8 | 9 | 10 | 11 | 12 | <% preload.forEach(x => { -%> 13 | 14 | <% }); -%> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
<%- body -%>
24 | 32 | <% assets.filter(x => x.endsWith('.js')).forEach(url => { -%> 33 | 34 | <% }); -%> 35 | <% if (env.GA_TRACKING_ID) { -%> 36 | 37 | <% } -%> 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/server/types/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Types 2 | 3 | - Use Data Loader pattern for the fields like `author` etc. 4 | - Implement the `Node` interface for those entity types that needs to be 5 | retrieved via the top-level `node(id)`, `nodes(ids)` GraphQL query fields. 6 | - Use helper functions for common fields such as dates etc. 7 | 8 | ```js 9 | import _ from 'lodash'; 10 | import { globalIdField } from 'graphql-relay'; 11 | import { GraphQLObjectType, GraphQLNonNull, GraphQLString } from 'graphql'; 12 | 13 | import { UserType } from './user'; 14 | import { nodeInterface } from '../node'; 15 | import { dateField } from '../utils'; 16 | 17 | export const StoryType = new GraphQLObjectType({ 18 | name: 'Story', 19 | interfaces: [nodeInterface], 20 | 21 | fields: { 22 | id: globalIdField(), 23 | 24 | author: { 25 | type: new GraphQLNonNull(UserType), 26 | resolve(self, args, ctx) { 27 | return ctx.userById.load(self.author_id); 28 | }, 29 | }, 30 | 31 | slug: { 32 | type: new GraphQLNonNull(GraphQLString), 33 | }, 34 | 35 | text: { 36 | type: new GraphQLNonNull(GraphQLString), 37 | }, 38 | 39 | createdAt: dateField(self => self.created_at), 40 | updatedAt: dateField(self => self.updated_at), 41 | }, 42 | }); 43 | ``` 44 | 45 | Note that the timestamp fields in the example above are designed to return dates 46 | in the current user's timezone. Also, it is possible to request pre-formatted 47 | dates (using [`moment.js`](https://momentjs.com/)). For example: 48 | 49 | ```graphql 50 | query { 51 | story(slug: "example") { 52 | createdAtISO: createdAt 53 | createdAtFmt: createdAt(format: "MMM Do YYYY, h:mm:ss a") 54 | } 55 | } 56 | 57 | # => { 58 | # data: { 59 | # story: { 60 | # createdAtISO: "2019-09-04T08:07:02Z", 61 | # createdAtFmt: "September 4th 2019, 11:07:02 am" 62 | # } 63 | # } 64 | # } 65 | ``` 66 | -------------------------------------------------------------------------------- /src/server/types/comment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { globalIdField } from 'graphql-relay'; 8 | import { 9 | GraphQLObjectType, 10 | GraphQLList, 11 | GraphQLNonNull, 12 | GraphQLInt, 13 | GraphQLString, 14 | } from 'graphql'; 15 | 16 | import { UserType } from './user'; 17 | import { StoryType } from './story'; 18 | import { nodeInterface } from '../node'; 19 | import { dateField } from '../utils'; 20 | 21 | export const CommentType = new GraphQLObjectType({ 22 | name: 'Comment', 23 | interfaces: [nodeInterface], 24 | 25 | fields: () => ({ 26 | id: globalIdField(), 27 | 28 | story: { 29 | type: new GraphQLNonNull(StoryType), 30 | resolve(self, args, ctx) { 31 | return ctx.storyById.load(self.story_id); 32 | }, 33 | }, 34 | 35 | parent: { 36 | type: CommentType, 37 | resolve(self, args, ctx) { 38 | return self.parent_id && ctx.commentById.load(self.parent_id); 39 | }, 40 | }, 41 | 42 | author: { 43 | type: new GraphQLNonNull(UserType), 44 | resolve(self, args, ctx) { 45 | return ctx.userById.load(self.author_id); 46 | }, 47 | }, 48 | 49 | comments: { 50 | type: new GraphQLList(CommentType), 51 | resolve(self, args, ctx) { 52 | return ctx.commentsByParentId.load(self.id); 53 | }, 54 | }, 55 | 56 | text: { 57 | type: GraphQLString, 58 | }, 59 | 60 | pointsCount: { 61 | type: new GraphQLNonNull(GraphQLInt), 62 | resolve(self, args, ctx) { 63 | return ctx.commentPointsCount.load(self.id); 64 | }, 65 | }, 66 | 67 | createdAt: dateField(self => self.created_at), 68 | updatedAt: dateField(self => self.updated_at), 69 | }), 70 | }); 71 | -------------------------------------------------------------------------------- /src/server/types/identity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import idx from 'idx'; 8 | import { GraphQLObjectType, GraphQLEnumType, GraphQLString } from 'graphql'; 9 | import { globalIdField } from 'graphql-relay'; 10 | 11 | export const IdentityType = new GraphQLObjectType({ 12 | name: 'Identity', 13 | 14 | fields: { 15 | id: globalIdField( 16 | 'Identity', 17 | self => `${self.provider}:${self.provider_id}`, 18 | ), 19 | 20 | provider: { 21 | type: new GraphQLEnumType({ 22 | name: 'AuthenticationProvider', 23 | values: { 24 | GOOGLE: { value: 'google' }, 25 | TWITTER: { value: 'twitter' }, 26 | FACEBOOK: { value: 'facebook' }, 27 | }, 28 | }), 29 | resolve: self => self.provider, 30 | }, 31 | 32 | providerId: { 33 | type: GraphQLString, 34 | resolve(self) { 35 | return self.provider_id; 36 | }, 37 | }, 38 | 39 | email: { 40 | type: GraphQLString, 41 | resolve(self, args, ctx) { 42 | if (!(ctx.user && (ctx.user.id === self.user_id || ctx.user.isAdmin))) { 43 | return null; 44 | } 45 | 46 | switch (self.provider) { 47 | case 'facebook': 48 | return idx(self, x => x.profile.email); 49 | default: 50 | return null; 51 | } 52 | }, 53 | }, 54 | 55 | displayName: { 56 | type: GraphQLString, 57 | resolve(self) { 58 | switch (self.provider) { 59 | case 'facebook': 60 | return idx(self, x => x.profile.name); 61 | default: 62 | return null; 63 | } 64 | }, 65 | }, 66 | 67 | photoURL: { 68 | type: GraphQLString, 69 | resolve(self) { 70 | switch (self.provider) { 71 | case 'google': 72 | return idx(self, x => x.profile.image.url); 73 | case 'facebook': 74 | return idx(self, x => x.profile.picture.data.url); 75 | default: 76 | return null; 77 | } 78 | }, 79 | }, 80 | 81 | profileURL: { 82 | type: GraphQLString, 83 | resolve(self) { 84 | switch (self.provider) { 85 | case 'google': 86 | return idx(self, x => x.profile.url); 87 | case 'facebook': 88 | return idx(self, x => x.profile.link); 89 | default: 90 | return null; 91 | } 92 | }, 93 | }, 94 | }, 95 | }); 96 | -------------------------------------------------------------------------------- /src/server/types/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | export * from './user'; 8 | export * from './identity'; 9 | export * from './story'; 10 | export * from './comment'; 11 | -------------------------------------------------------------------------------- /src/server/types/story.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import _ from 'lodash'; 8 | import { globalIdField } from 'graphql-relay'; 9 | import { 10 | GraphQLObjectType, 11 | GraphQLList, 12 | GraphQLBoolean, 13 | GraphQLNonNull, 14 | GraphQLInt, 15 | GraphQLString, 16 | } from 'graphql'; 17 | 18 | import { UserType } from './user'; 19 | import { CommentType } from './comment'; 20 | import { nodeInterface } from '../node'; 21 | import { dateField } from '../utils'; 22 | 23 | export const StoryType = new GraphQLObjectType({ 24 | name: 'Story', 25 | interfaces: [nodeInterface], 26 | 27 | fields: { 28 | id: globalIdField(), 29 | 30 | author: { 31 | type: new GraphQLNonNull(UserType), 32 | resolve(self, args, ctx) { 33 | return ctx.userById.load(self.author_id); 34 | }, 35 | }, 36 | 37 | slug: { 38 | type: new GraphQLNonNull(GraphQLString), 39 | }, 40 | 41 | title: { 42 | type: new GraphQLNonNull(GraphQLString), 43 | }, 44 | 45 | text: { 46 | type: new GraphQLNonNull(GraphQLString), 47 | args: { 48 | truncate: { type: GraphQLInt }, 49 | }, 50 | resolve(self, args) { 51 | return args.truncate 52 | ? _.truncate(self.text, { length: args.truncate }) 53 | : self.text; 54 | }, 55 | }, 56 | 57 | isURL: { 58 | type: new GraphQLNonNull(GraphQLBoolean), 59 | resolve(self) { 60 | return self.is_url; 61 | }, 62 | }, 63 | 64 | comments: { 65 | type: new GraphQLList(CommentType), 66 | resolve(self, args, ctx) { 67 | return ctx.commentsByStoryId.load(self.id); 68 | }, 69 | }, 70 | 71 | pointsCount: { 72 | type: new GraphQLNonNull(GraphQLInt), 73 | resolve(self, args, ctx) { 74 | return ctx.storyPointsCount.load(self.id); 75 | }, 76 | }, 77 | 78 | pointGiven: { 79 | type: new GraphQLNonNull(GraphQLBoolean), 80 | resolve(self, args, ctx) { 81 | return ctx.user ? ctx.storyPointGiven.load(self.id) : false; 82 | }, 83 | }, 84 | 85 | commentsCount: { 86 | type: new GraphQLNonNull(GraphQLInt), 87 | resolve(self, args, ctx) { 88 | return ctx.storyCommentsCount.load(self.id); 89 | }, 90 | }, 91 | 92 | createdAt: dateField(self => self.created_at), 93 | updatedAt: dateField(self => self.updated_at), 94 | }, 95 | }); 96 | -------------------------------------------------------------------------------- /src/server/types/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import firebase from 'firebase-admin'; 8 | import { globalIdField } from 'graphql-relay'; 9 | import { 10 | GraphQLObjectType, 11 | GraphQLList, 12 | GraphQLNonNull, 13 | GraphQLString, 14 | GraphQLBoolean, 15 | } from 'graphql'; 16 | 17 | import { IdentityType } from './identity'; 18 | import { nodeInterface } from '../node'; 19 | import { dateField } from '../utils'; 20 | 21 | export const UserType = new GraphQLObjectType({ 22 | name: 'User', 23 | interfaces: [nodeInterface], 24 | 25 | fields: { 26 | id: globalIdField(), 27 | 28 | username: { 29 | type: new GraphQLNonNull(GraphQLString), 30 | }, 31 | 32 | email: { 33 | type: GraphQLString, 34 | resolve(self, args, ctx) { 35 | return ctx.user && (ctx.user.id === self.id || ctx.user.isAdmin) 36 | ? self.email 37 | : null; 38 | }, 39 | }, 40 | 41 | displayName: { 42 | type: GraphQLString, 43 | resolve(self) { 44 | return self.display_name; 45 | }, 46 | }, 47 | 48 | photoURL: { 49 | type: GraphQLString, 50 | resolve(self) { 51 | return self.photo_url; 52 | }, 53 | }, 54 | 55 | timeZone: { 56 | type: GraphQLString, 57 | resolve(self) { 58 | return self.time_zone; 59 | }, 60 | }, 61 | 62 | identities: { 63 | type: new GraphQLList(IdentityType), 64 | resolve(self, args, ctx) { 65 | return ctx.identitiesByUserId.load(self.id); 66 | }, 67 | }, 68 | 69 | isAdmin: { 70 | type: GraphQLBoolean, 71 | resolve(self, args, ctx) { 72 | return ctx.user && ctx.user.id === self.id 73 | ? ctx.user.isAdmin || false 74 | : self.is_admin; 75 | }, 76 | }, 77 | 78 | firebaseToken: { 79 | type: GraphQLString, 80 | resolve(self, args, ctx) { 81 | return ctx.user && ctx.user.id === self.id 82 | ? firebase.auth().createCustomToken(self.id) 83 | : null; 84 | }, 85 | }, 86 | 87 | createdAt: dateField(x => x.created_at), 88 | updatedAt: dateField(x => x.updated_at), 89 | lastLoginAt: dateField(x => x.last_login_at), 90 | }, 91 | }); 92 | -------------------------------------------------------------------------------- /src/server/utils/fields.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import moment from 'moment-timezone'; 8 | import { GraphQLNonNull, GraphQLString, GraphQLInt } from 'graphql'; 9 | 10 | const dateFieldArgs = { 11 | format: { type: GraphQLString }, 12 | }; 13 | 14 | const dateFieldResolve = (resolve, self, args, ctx) => { 15 | let date = resolve(self); 16 | 17 | if (!date) { 18 | return null; 19 | } 20 | 21 | const timeZone = ctx.user && ctx.user.timeZone; 22 | 23 | if (timeZone) { 24 | date = moment(date).tz(timeZone); 25 | } else { 26 | date = moment(date); 27 | } 28 | 29 | return date.format(args.format); 30 | }; 31 | 32 | /** 33 | * Creates the configuration for a date/time field with support of format and time 34 | * zone. 35 | */ 36 | export function dateField(resolve) { 37 | return { 38 | type: GraphQLString, 39 | args: dateFieldArgs, 40 | resolve: dateFieldResolve.bind(undefined, resolve), 41 | }; 42 | } 43 | 44 | export const countField = { 45 | type: new GraphQLNonNull(GraphQLInt), 46 | resolve(self) { 47 | return self.query.count().then(x => x[0].count); 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/server/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | export * from './map'; 8 | export * from './relay'; 9 | export * from './type'; 10 | export * from './fields'; 11 | export * from './username'; 12 | export * from './user'; 13 | -------------------------------------------------------------------------------- /src/server/utils/map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | export function mapTo(keys, keyFn) { 8 | return rows => { 9 | const group = new Map(keys.map(key => [key, null])); 10 | rows.forEach(row => group.set(keyFn(row), row)); 11 | return Array.from(group.values()); 12 | }; 13 | } 14 | 15 | export function mapToMany(keys, keyFn) { 16 | return rows => { 17 | const group = new Map(keys.map(key => [key, []])); 18 | rows.forEach(row => (group.get(keyFn(row)) || []).push(row)); 19 | return Array.from(group.values()); 20 | }; 21 | } 22 | 23 | export function mapToValues(keys, keyFn, valueFn, defaultValue = null) { 24 | return rows => { 25 | const group = new Map(keys.map(key => [key, defaultValue])); 26 | rows.forEach(row => group.set(keyFn(row), valueFn(row))); 27 | return Array.from(group.values()); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/server/utils/relay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { fromGlobalId as parse } from 'graphql-relay'; 8 | 9 | export function fromGlobalId(globalId, expectedType) { 10 | const { id, type } = parse(globalId); 11 | 12 | if (expectedType && type !== expectedType) { 13 | throw new Error( 14 | `Expected an ID of type '${expectedType}' but got '${type}'.`, 15 | ); 16 | } 17 | 18 | return id; 19 | } 20 | -------------------------------------------------------------------------------- /src/server/utils/type.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | export function assignType(type) { 8 | return obj => { 9 | // eslint-disable-next-line no-underscore-dangle, no-param-reassign 10 | if (obj) obj.__type = type; 11 | return obj; 12 | }; 13 | } 14 | 15 | export function getType(obj) { 16 | // eslint-disable-next-line no-underscore-dangle 17 | return obj ? obj.__type : undefined; 18 | } 19 | -------------------------------------------------------------------------------- /src/server/utils/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import idx from 'idx'; 8 | 9 | import db from '../db'; 10 | import { generateUsername } from './username'; 11 | 12 | /** 13 | * Finds a user matching the provided Passport.js credentials. If user not 14 | * found, it attempts to create a new user account. 15 | */ 16 | export async function upsertUser(profile, credentials) { 17 | const identityKeys = { 18 | 'user_identities.provider': profile.provider, 19 | 'user_identities.provider_id': profile.id, 20 | }; 21 | 22 | const email = idx(profile, x => x.emails[0].value); 23 | let photo = idx(profile, x => x.photos[0].value); 24 | if (photo && profile.provider === 'facebook') { 25 | photo = `https://graph.facebook.com/${profile.id}/picture?type=large`; 26 | } 27 | 28 | let user = await db 29 | .table('user_identities') 30 | .leftJoin('users', 'users.id', 'user_identities.user_id') 31 | .where(identityKeys) 32 | .select('users.*') 33 | .first(); 34 | 35 | if (user) { 36 | await Promise.all([ 37 | db 38 | .table('user_identities') 39 | .where(identityKeys) 40 | .update({ 41 | credentials: JSON.stringify(credentials), 42 | profile: JSON.stringify(profile._json), 43 | updated_at: db.fn.now(), 44 | }), 45 | db 46 | .table('users') 47 | .where({ id: user.id }) 48 | .update({ last_login_at: db.fn.now() }), 49 | photo && 50 | db 51 | .table('users') 52 | .where({ id: user.id }) 53 | .andWhere(x => 54 | x 55 | .where('photo_url', 'like', '%googleusercontent.com/%') 56 | .orWhere('photo_url', 'like', '%facebook.com/%') 57 | .orWhere('photo_url', 'like', '%fbcdn.net/%') 58 | .orWhere('photo_url', 'like', '%fbsbx.com/%'), 59 | ) 60 | .update({ 61 | photo_url: photo, 62 | }), 63 | ]); 64 | } else { 65 | user = await db 66 | .table('users') 67 | .where(email ? { email } : db.raw('false')) 68 | .first(); 69 | 70 | if (!user) { 71 | [user] = await db 72 | .table('users') 73 | .insert({ 74 | email, 75 | username: profile.username || (await generateUsername(email)), 76 | display_name: profile.displayName, 77 | photo_url: photo, 78 | }) 79 | .returning('*'); 80 | } 81 | 82 | await db.table('user_identities').insert({ 83 | user_id: user.id, 84 | provider: profile.provider, 85 | provider_id: profile.id, 86 | profile: JSON.stringify(profile._json), 87 | credentials: JSON.stringify(credentials), 88 | }); 89 | } 90 | 91 | return user; 92 | } 93 | -------------------------------------------------------------------------------- /src/server/utils/username.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import db from '../db'; 8 | 9 | const USERNAME_REGEX = /^(?=.{2,50}$)(?![_.])(?!.*[_.]{2})[a-zA-Z0-9._]+(? 30 | 31 | 32 | My Account 33 | 34 | 35 | Welcome, {props.user && props.user.displayName}! 36 | 37 | 38 |
39 | ); 40 | } 41 | 42 | export default createFragmentContainer( 43 | Account, 44 | graphql` 45 | fragment Account on Query { 46 | me { 47 | id 48 | username 49 | displayName 50 | photoURL 51 | } 52 | } 53 | `, 54 | ); 55 | -------------------------------------------------------------------------------- /src/user/Login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Container from '@material-ui/core/Container'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | 11 | import AppBar from '../common/AppBar'; 12 | import LoginForm from '../common/LoginForm'; 13 | 14 | const useStyles = makeStyles(theme => ({ 15 | root: { 16 | display: 'table', 17 | minHeight: '100vh', 18 | paddingTop: 64, 19 | }, 20 | form: { 21 | display: 'table-cell', 22 | verticalAlign: 'middle', 23 | }, 24 | })); 25 | 26 | function Login() { 27 | const s = useStyles(); 28 | 29 | function handleLoginComplete({ user, relay } = {}) { 30 | console.log('user:', user); 31 | } 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | export default Login; 44 | -------------------------------------------------------------------------------- /src/user/UserProfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import Avatar from '@material-ui/core/Avatar'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import { makeStyles } from '@material-ui/core/styles'; 11 | import { createFragmentContainer, graphql } from 'react-relay'; 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | root: { 15 | ...theme.mixins.content, 16 | }, 17 | })); 18 | 19 | function UserProfile(props) { 20 | const { data: user } = props; 21 | const s = useStyles(); 22 | 23 | return ( 24 |
25 | 30 | 35 | {user.displayName} 36 | 37 | 38 | Lorem Ipsum is simply dummy text of the printing and typesetting 39 | industry. Lorem Ipsum has been the industry's standard dummy text 40 | ever since the 1500s, when an unknown printer took a galley of type and 41 | scrambled it to make a type specimen book. It has survived not only five 42 | centuries, but also the leap into electronic typesetting, remaining 43 | essentially unchanged. It was popularised in the 1960s with the release 44 | of Letraset sheets containing Lorem Ipsum passages, and more recently 45 | with desktop publishing software like Aldus PageMaker including versions 46 | of Lorem Ipsum. 47 | 48 |
49 | ); 50 | } 51 | 52 | export default createFragmentContainer( 53 | UserProfile, 54 | graphql` 55 | fragment UserProfile on User { 56 | id 57 | username 58 | displayName 59 | photoURL 60 | } 61 | `, 62 | ); 63 | -------------------------------------------------------------------------------- /src/user/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import React from 'react'; 8 | import { graphql } from 'relay-runtime'; 9 | import Layout from '../common/Layout'; 10 | import Login from './Login'; 11 | 12 | export default [ 13 | { 14 | path: '/login', 15 | render: (_, data, { config }) => ({ 16 | title: `Sign In to ${config.app.name}`, 17 | component: , 18 | }), 19 | }, 20 | { 21 | path: '/@:username', 22 | components: () => [ 23 | import(/* webpackChunkName: 'user-profile' */ './UserProfile'), 24 | ], 25 | query: graphql` 26 | query userProfileQuery($username: String!) { 27 | ...Layout_data 28 | user(username: $username) { 29 | displayName 30 | ...UserProfile 31 | } 32 | } 33 | `, 34 | render: ([UserProfile], data, { config }) => ({ 35 | title: `${data.user.displayName} • ${config.app.name}`, 36 | component: ( 37 | 38 | 39 | 40 | ), 41 | chunks: ['user-profile'], 42 | }), 43 | }, 44 | { 45 | path: '/account', 46 | components: () => [import(/* webpackChunkName: 'account' */ './Account')], 47 | query: graphql` 48 | query userQuery { 49 | ...Layout_data 50 | ...Account 51 | } 52 | `, 53 | render: ([Account], data, { config }) => ({ 54 | title: `My Account • ${config.app.name}`, 55 | component: ( 56 | 57 | 58 | 59 | ), 60 | chunks: ['account'], 61 | }), 62 | }, 63 | ]; 64 | -------------------------------------------------------------------------------- /src/utils/env.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | export const canUseDOM = !!( 8 | typeof window !== 'undefined' && 9 | window.document && 10 | window.document.createElement 11 | ); 12 | -------------------------------------------------------------------------------- /src/utils/gtag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { canUseDOM } from './env'; 8 | 9 | export function gtag() { 10 | if (canUseDOM && window.dataLayer) { 11 | window.dataLayer.push(arguments); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | export * from './env'; 8 | export * from './gtag'; 9 | export * from './openWindow'; 10 | export { onScroll, getScrollPosition } from './scrolling'; 11 | -------------------------------------------------------------------------------- /src/utils/loading.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | let count = 0; 8 | const listeners = new Set(); 9 | 10 | let notification; 11 | 12 | export default { 13 | listen(cb) { 14 | listeners.add(cb); 15 | return () => listeners.delete(cb); 16 | }, 17 | notifyStart() { 18 | if (count++ === 0) { 19 | notification = setTimeout(() => { 20 | listeners.forEach(x => x(true /* loading is in progress */)); 21 | }, 200 /* delay start loading notification for 200ms */); 22 | } 23 | }, 24 | notifyStop() { 25 | listeners.forEach(x => x(false /* loading is no longer in progress */)); 26 | if (--count === 0) { 27 | clearTimeout(notification); 28 | listeners.forEach(x => x(false /* loading is no longer in progress */)); 29 | } 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/openWindow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | function getWindowFeataures(options = {}) { 8 | const width = options.width || 600; 9 | const height = options.height || 600; 10 | const { screenLeft, screenTop, innerWidth, innerHeight, screen } = window; 11 | const html = window.document.documentElement; 12 | 13 | const dualScreenLeft = screenLeft !== undefined ? screenLeft : screen.left; 14 | const dualScreenTop = screenTop !== undefined ? screenTop : screen.top; 15 | const w = innerWidth || html.clientWidth || screen.width; 16 | const h = innerHeight || html.clientHeight || screen.height; 17 | 18 | const config = { 19 | width, 20 | height, 21 | left: w / 2 - width / 2 + dualScreenLeft, 22 | top: h / 2 - height / 2 + dualScreenTop, 23 | }; 24 | 25 | return Object.keys(config) 26 | .map(key => `${key}=${config[key]}`) 27 | .join(','); 28 | } 29 | 30 | export function openWindow(uri, { onPostMessage, ...options } = {}) { 31 | const win = window.open(uri, null, getWindowFeataures(options)); 32 | 33 | let executor; 34 | 35 | const onResolve = data => { 36 | window.removeEventListener('message', onPostMessageWrapper); 37 | 38 | if (executor) { 39 | win.close(); 40 | executor.resolve(data); 41 | executor = null; 42 | } 43 | }; 44 | 45 | const onPostMessageWrapper = event => { 46 | if (onPostMessage) { 47 | const result = onPostMessage(event); 48 | if (result) onResolve(result); 49 | } 50 | }; 51 | 52 | window.addEventListener('message', onPostMessageWrapper, true); 53 | 54 | return new Promise(resolve => { 55 | executor = { resolve }; 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/scrolling.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Starter Kit for Firebase 3 | * https://github.com/kriasoft/react-firebase-starter 4 | * Copyright (c) 2015-present Kriasoft | MIT License 5 | */ 6 | 7 | import { canUseDOM } from './env'; 8 | 9 | const listeners = new Set(); 10 | const scrollPositions = new Map(); 11 | 12 | let last_known_scroll_position = 0; 13 | let ticking = false; 14 | 15 | let history; 16 | 17 | if (canUseDOM) { 18 | window.addEventListener('scroll', () => { 19 | last_known_scroll_position = window.scrollY; 20 | if (!ticking) { 21 | window.requestAnimationFrame(() => { 22 | if (history) { 23 | scrollPositions.set(history.location.key, last_known_scroll_position); 24 | } 25 | listeners.forEach(cb => cb(last_known_scroll_position)); 26 | ticking = false; 27 | }); 28 | ticking = true; 29 | } 30 | }); 31 | } 32 | 33 | export function onScroll(cb) { 34 | listeners.add(cb); 35 | return () => listeners.delete(cb); 36 | } 37 | 38 | export function setHistory(browserHistory) { 39 | history = browserHistory; 40 | } 41 | 42 | export function getScrollPosition(locationKey) { 43 | return scrollPositions.get(locationKey); 44 | } 45 | -------------------------------------------------------------------------------- /ssl/README.md: -------------------------------------------------------------------------------- 1 | # SSL Certificates 2 | 3 | If your database server requires SSL/TLS certificates, put them in this folder. For example: 4 | 5 | ``` 6 | ssl/prod.client-cert.pem 7 | ssl/prod.client-key.pem 8 | ssl/prod.server-ca.pem 9 | 10 | ssl/test.client-cert.pem 11 | ssl/test.client-key.pem 12 | ssl/test.server-ca.pem 13 | ``` 14 | 15 | For more information visit: 16 | 17 | - https://cloud.google.com/sql/docs/postgres/configure-ssl-instance 18 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | service firebase.storage { 2 | match /b/{bucket}/o { 3 | match /{allPaths=**} { 4 | allow read, write: if request.auth!=null; 5 | } 6 | } 7 | } 8 | --------------------------------------------------------------------------------