38
.env.test
Normal file
@@ -0,0 +1,38 @@
|
||||
MEDIA=/Users/luke/Testing/media
|
||||
DATA=/Users/luke/Testing/data
|
||||
MAILJET_KEY="4d2a0f6deb20a7890afee2daf595c22d"
|
||||
MAILJET_SECRET="fb74f682d68bf6b05ab626c0ac93a6da"
|
||||
GOOGLE_ANALYTICS="UA-45563211-1"
|
||||
STAGE=test
|
||||
TAG=1.7
|
||||
DOMAIN_NAME="openmhz.test"
|
||||
PROTOCOL="http://"
|
||||
|
||||
|
||||
REACT_APP_BACKEND_SERVER="${PROTOCOL}api.${DOMAIN_NAME}"
|
||||
REACT_APP_FRONTEND_SERVER="${PROTOCOL}${DOMAIN_NAME}"
|
||||
REACT_APP_SOCKET_SERVER=wss://socket.${DOMAIN_NAME}
|
||||
REACT_APP_ACCOUNT_SERVER="${PROTOCOL}account.${DOMAIN_NAME}"
|
||||
REACT_APP_ADMIN_SERVER="${PROTOCOL}admin.${DOMAIN_NAME}"
|
||||
REACT_APP_COOKIE_DOMAIN=".${DOMAIN_NAME}"
|
||||
REACT_APP_ADMIN_EMAIL=luke@robotastic.com
|
||||
REACT_APP_SITE_NAME=OpenMHz
|
||||
REACT_APP_ARCHIVE_DAYS=30
|
||||
STRIPE_PUBLISHABLE_KEY=""
|
||||
STRIPE_SECRET_KEY="sk_test_S5kfQwdyRDgKtu9FQYpPh3B3"
|
||||
BACKEND_SERVER="${PROTOCOL}api.${DOMAIN_NAME}"
|
||||
FRONTEND_SERVER="${PROTOCOL}${DOMAIN_NAME}"
|
||||
ADMIN_SERVER="${PROTOCOL}admin.${DOMAIN_NAME}"
|
||||
SOCKET_SERVER="wss://socket.${DOMAIN_NAME}"
|
||||
ACCOUNT_SERVER="${PROTOCOL}account.${DOMAIN_NAME}"
|
||||
COOKIE_DOMAIN=".${DOMAIN_NAME}"
|
||||
SITE_NAME="OpenMHz"
|
||||
ADMIN_EMAIL="luke@robotastic.com"
|
||||
S3_PROFILE='wasabi-account'
|
||||
S3_ENPOINT='s3.us-west-1.wasabisys.com'
|
||||
FREE_PLAN=0
|
||||
PRO_PLAN=10
|
||||
FREE_PLAN_PRICE=0
|
||||
PRO_PLAN_PRICE=15
|
||||
FREE_PLAN_ARCHIVE=7
|
||||
PRO_PLAN_ARCHIVE=30
|
||||
11
.gitignore
vendored
@@ -1,2 +1,9 @@
|
||||
docker-prod.sh
|
||||
docker-test.sh
|
||||
.DS_Store
|
||||
prod.env
|
||||
test.env
|
||||
account/.eslintcache
|
||||
account/.eslintcache
|
||||
admin/.eslintcache
|
||||
frontend/.eslintcache
|
||||
admin/.eslintcache
|
||||
data/*
|
||||
|
||||
206
README.md
@@ -14,6 +14,9 @@ There are a bunch of experiments lurking in the code. There is some code for add
|
||||
|
||||
I haven't done a great job of keeping all the packages up to date... and I never got around to adding tests. Both of these would be great things for folks to go after.
|
||||
|
||||
## PROD vs DEV Env
|
||||
The Prod environment expects you to be using HTTPS for your domains. It is pretty easy to use Let's Encrypt to grab Certs for the domains you are using. If not, just run things using the `docker-test.sh` script.
|
||||
|
||||
## Path Forward
|
||||
It would be great to get the code to a place where there is base code and people can add customization on top of that. They would probably fork the base code, add additional features and design and then rebaseline the code as new things are added to the mainline.
|
||||
|
||||
@@ -26,49 +29,72 @@ There are a lot of different components that make up the system. All of the serv
|
||||
- **mongo**: all of the metadata around calls is stored here, along with the user and system information.
|
||||
- **nginx**: proxies all of the calls to the correct server and handles the HTTPS certs.
|
||||
|
||||
## Easy Install
|
||||
|
||||
*NOTE: This may no longers be the easy method... I haven't updated the Ansible scripts in a while. Use at your own peril!! PRs wanted if you fix them up.*
|
||||
I put together an [Ansible Script](https://github.com/openmhz/trunk-server-install) to help make it easier to setup an OpenMHz server. You still need to get yourself a Droplet from Digital Ocean, a Domain Name and some storage. The scripts helps download and build everything.
|
||||
|
||||
## Operations
|
||||
You can run things in 2 different modes, **test** and **prod**. The big difference is that **prod** expects all of the calls to be https and gets cranky when they are not. I have gotten **test** to run fine on my laptop, so that is probably a good starting point.
|
||||
|
||||
You can run things in 2 different modes, `test` and `prod`. The big difference is that `prod` expects all of the calls to be https and gets cranky when they are not. I have gotten `test` to run fine on my laptop, so that is probably a good starting point.
|
||||
### DNS Entries
|
||||
You should have a domain name pointing to the IP address of the server you are going to use.
|
||||
CNAMEs also need to be created for the various services. Create the CNAMEs below with your DNS host:
|
||||
- api
|
||||
- account
|
||||
- admin
|
||||
|
||||
Currently, both `test` and `prod` expect to use S3-based storage instead of local storage. Switching to use local storage would be relatively easy - but for the sake of testing, let's just say use something S3-compatible. Make sure that ~/.aws/credentials has the credentials you'd like to use with your S3-compatible storage provider.. IE:
|
||||
After doing this, you should have the following domains: `api.domain.com`, `account.domain.com`, `admin.domain.com`
|
||||
|
||||
### S3 Storage
|
||||
Currently, both **test** and **prod** expect to use S3-based storage instead of local storage. Switching to use local storage would be relatively easy - but for the sake of testing, let's just say use something S3-compatible. Make sure that ~/.aws/credentials has the credentials you'd like to use with your S3-compatible storage provider.. IE:
|
||||
```
|
||||
[default]
|
||||
aws_access_key_id = [..]
|
||||
aws_secret_access_key = [..]
|
||||
```
|
||||
|
||||
Copy docker-test.sh.example to docker-test.sh, and/or docker-prod.sh.example to docker-prod.sh.
|
||||
### Configure
|
||||
The configurations for both **test** and **prod** come from environment variable files that are read in before the containers are started.
|
||||
Copy the example files and fill in the required info:
|
||||
|
||||
```bash
|
||||
cp test.env.example test.env
|
||||
cp prod.env.example prod.env
|
||||
```
|
||||
|
||||
Fill in:
|
||||
- MailJet information
|
||||
- S3 information
|
||||
- Site name
|
||||
- Admin Email (The email will appear to come from this email. You should make sure it matches the domain MailJet is configured for)
|
||||
- How many days calls will be archived for.... this is just a UI thing, you need to create some S3 rules to make sure they are deleted
|
||||
|
||||
|
||||
### Scripts
|
||||
|
||||
`./docker-test.sh` - sets up docker-compose to run with the correct environment variables for testing
|
||||
|
||||
`./docker-prod.sh` - sets up docker-compose to run with the correct environment variables for production
|
||||
|
||||
Fill out the Environment variables in both these files. S3_PROFILE should match the profile that is defined in the aws/credentials file. You don't need to worry about the Stripe stuff, since that is dormant.
|
||||
|
||||
If you are doing frontend work, you can turn off the de-obfuscation. To do this, edit the Dockerfile for the server you are working with. Change the CMD to:
|
||||
`CMD ["npm", "run", "start:dev"]` Just remember to turn it off when you make the code public.
|
||||
|
||||
|
||||
|
||||
### Hosting
|
||||
I use Digital Ocean and they have been pretty great. If you need hosting, give them a try and use my referal code: https://m.do.co/c/402fa446f7a6
|
||||
### Service I Like
|
||||
- 💻 I use Digital Ocean and they have been pretty great. If you need hosting, give them a try and use my referal code: https://m.do.co/c/402fa446f7a6
|
||||
You get $100 of credit to use in 60 days.
|
||||
|
||||
If you need storage, give Wasabi a try. The have been mostly reliable and you can't beat the price: https://wasabi.com
|
||||
- 💾 If you need storage, give Wasabi a try. The have been mostly reliable and you can't beat the price: https://wasabi.com
|
||||
|
||||
- 📨 I use Mailjet... and you need to also. It makes it easy to send out email address confirmations: https://www.mailjet.com
|
||||
|
||||
- 🔒 I use Let's Encrypt. It works.
|
||||
|
||||
|
||||
### HTTPS
|
||||
I use Let's Encrypt. It works.
|
||||
## Local Testing
|
||||
|
||||
### For Local Testing:
|
||||
- In root dir, run: `./docker-test.sh build`
|
||||
- `./docker-test.sh up -d`
|
||||
### Local DNS
|
||||
|
||||
You can then browser to:
|
||||
- openmhz.test
|
||||
- admin.openmhz.test
|
||||
It helps to have similar subdomains mapping to localhost/127.0.0.1. I used the domain `openmhz.test`. Feel free to use this, or come up with something clever. If you do something different, make sure you use that instead in the commands below.
|
||||
|
||||
Make sure the following is in: */etc/hosts*
|
||||
On MacOS, make the changes in: */etc/hosts*
|
||||
|
||||
````
|
||||
##
|
||||
@@ -78,25 +104,133 @@ Make sure the following is in: */etc/hosts*
|
||||
# when the system is booting. Do not change this entry.
|
||||
##
|
||||
127.0.0.1 localhost
|
||||
127.0.0.1 openmhz.test api.openmhz.test admin.openmhz.test media.openmhz.test
|
||||
127.0.0.1 openmhz.test account.openmhz.test api.openmhz.test admin.openmhz.test media.openmhz.test
|
||||
````
|
||||
Then load these values into local DNS: `sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder`
|
||||
|
||||
|
||||
- In root dir, run: `./docker-test.sh build`
|
||||
- `./docker-test.sh up -d`
|
||||
|
||||
You can then browser to:
|
||||
- openmhz.test
|
||||
- admin.openmhz.test
|
||||
|
||||
*Interesting Note*: Safari 13+ does not like the **.test** TLD and doesn't seem to want to store cookies from the TLD. It seems to work fine in production when you are using a real TLD. I guess use Chrome for local testing or a different TLD for testing, like local.
|
||||
https://stackoverflow.com/questions/62023857/sharing-cookies-across-test-sub-domains-in-safari-13-not-possible
|
||||
|
||||
### Debugging React Apps using Hot Reloading
|
||||
If you are trying to make changes to any of react frontends, it is a huge pain to have to compile to site and rebuild the container each time you make a change. Instead, simply run the react app in development mode. This will work for the frontends for the:
|
||||
- admin frontend
|
||||
- account frontend
|
||||
- frontend... frontend
|
||||
|
||||
First, start up all of the containers as described above. You will still the backend APIs they provide.
|
||||
|
||||
Now go into the respective sub directory for the component you are interested in and run:
|
||||
````
|
||||
source ../test.env
|
||||
yarn install #only need to do this the first time, it installs the Node packages locally
|
||||
yarn start
|
||||
````
|
||||
|
||||
### Default user name, for Test:
|
||||
This should build the frontend and open a browser. In order to have all the cookies work correctly, you have to use the same domain name. Make sure you have setup the local domains as described above. Then goto the base domain, for me that is openmhz.test, at port 3000 `openmhz.test:3000`
|
||||
|
||||
- test@test.com
|
||||
- password: password
|
||||
## Upgrading a "Frontend" server
|
||||
|
||||
https://create-react-app.dev/docs/updating-to-new-releases/
|
||||
|
||||
|
||||
### DNS Entries
|
||||
## Managing MongoDB
|
||||
|
||||
CNAMEs need to be created for the various services. Create the CNAMEs below with your DNS host:
|
||||
- api
|
||||
- account
|
||||
- media
|
||||
- admin
|
||||
MongoDB is used in the backend to store data. It is pretty fast, flexible and has worked well enough for me.
|
||||
All of the files that MongoDB uses to store the DB are in the */data* directory, which gets mapped into the container.
|
||||
Mapping this directory makes sure that the data persists each time you run the mongo container.
|
||||
|
||||
After doing this, you should have the following domains: api.domain.com, account.domain.com, media.domain.com, admin.domain.compose
|
||||
### Working with the MongoDB Container
|
||||
|
||||
### Mailjet
|
||||
Sign up for MailJet!
|
||||
https://www.mailjet.com
|
||||
From the Host OS run:
|
||||
```bash
|
||||
docker exec -i -t $(docker ps -a | grep mongo | awk '{print $1}') /bin/bash
|
||||
```
|
||||
Then launch the `mongo` CLI tool and find any users you have created
|
||||
```bash
|
||||
mongo
|
||||
use scanner
|
||||
db.users.find()
|
||||
```
|
||||
|
||||
Now swap out the ObjectId for the user, and then run this command. It will make it so you don't need to confirm the email.
|
||||
|
||||
```bash
|
||||
db.users.updateOne(
|
||||
{ "_id" : ObjectId("63a620d0a63b087b005f6726") },
|
||||
{
|
||||
$set: { "confirmEmail" : true },
|
||||
$currentDate: { lastModified: true }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
There are a few scripts included with the container:
|
||||
- **clean.js** This script removes all Calls that are over 30 days old
|
||||
- **totals.js** Lists different system stats
|
||||
|
||||
### Compact a collection
|
||||
When you run clean.js it doesn't actually remove the files off storge. You can use this command from the mongo cli tool.
|
||||
|
||||
First, launch the tool: `mongo`
|
||||
Then switch to the scanner db: `use scanner`
|
||||
And then the compact command on the **Calls** collection:
|
||||
|
||||
```
|
||||
db.runCommand({compact:'calls'})
|
||||
```
|
||||
This blocks all calls to the DB, so the site will not work while this is being run.
|
||||
|
||||
### Add an Index
|
||||
Adding an index will make it quicker to search calls by date, talkgroup and whether there are stars.
|
||||
|
||||
First, launch the tool: `mongo`
|
||||
Then switch to the scanner db: `use scanner`
|
||||
And then add an index:
|
||||
|
||||
```
|
||||
db.calls.createIndex( {shortName: 1, time: -1, talkgroupNum: 1})
|
||||
```
|
||||
|
||||
### Upgrading MongoDB
|
||||
It is a huge pain to upgrade MongoDB in place. It turns out to be easier to dump backup of the database, wipe everything out and then restore into a new database for the latest version of Mongo.
|
||||
|
||||
Rough Playbook (use common sense, I may not have this exact):
|
||||
- get into the shell of the mongo container
|
||||
- `mongodump --db scanner —out /data/db/backup`
|
||||
- exit contianer and go back to host machine
|
||||
- `cd data/db`
|
||||
- `rm *` erase everything... but not the sub-directories because that is where the backup is
|
||||
- upgrade to the latest version of mongo
|
||||
- build and launch the mongo container, which will create an empty DB
|
||||
- get into the shell of the mongo container
|
||||
- `mongorestore --db scanner --drop /data/db/backup/`
|
||||
|
||||
## Setting up Logging
|
||||
I have had good luck with Loggly. Their free tier provides enough capabilities for most small sites.
|
||||
The Doocker Logging Driver works well and is easy to install:
|
||||
|
||||
https://documentation.solarwinds.com/en/Success_Center/loggly/Content/admin/docker-logging-driver.htm
|
||||
|
||||
|
||||
## Move a site to a new server
|
||||
|
||||
Here is the general list of things to do:
|
||||
- Copy over ~/.aws
|
||||
- Copy over ~/.secrets
|
||||
- Do a mongodump on old machine
|
||||
- Scp the mongo backup to the new machine
|
||||
- Launch the MongoDB container on the new machine
|
||||
- Do a mongorestore
|
||||
- Launch all the conatiners
|
||||
- Change the Floating IP to point to the new machine
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets":
|
||||
["@babel/preset-env",
|
||||
"@babel/react"],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
["@babel/plugin-transform-runtime", {
|
||||
"regenerator": true
|
||||
}],
|
||||
["lodash", { "id": ["lodash", "semantic-ui-react"] }]
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
node_modules
|
||||
lib
|
||||
public
|
||||
/node_modules
|
||||
/build
|
||||
1
account/.eslintcache
Normal file
60
account/.gitignore
vendored
@@ -1,37 +1,23 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
lib/
|
||||
uploads/
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
assets
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { configure } from '@storybook/react';
|
||||
|
||||
function loadStories() {
|
||||
require('../src/client/stories/index.js');
|
||||
// You can require as many stories as you need.
|
||||
}
|
||||
|
||||
configure(loadStories, module);
|
||||
@@ -1,21 +1,28 @@
|
||||
# docker build -t trunk-server-frontend .
|
||||
# do a `git pull` in trunk-server-frontend to update
|
||||
# build environment
|
||||
FROM node:13.12.0-alpine as build
|
||||
WORKDIR /app
|
||||
ENV PATH /app/node_modules/.bin:$PATH
|
||||
ARG REACT_APP_BACKEND_SERVER
|
||||
ARG REACT_APP_ADMIN_SERVER
|
||||
ARG REACT_APP_ACCOUNT_SERVER
|
||||
ARG REACT_APP_FRONTEND_SERVER
|
||||
ARG REACT_APP_SITE_NAME
|
||||
ARG REACT_APP_ADMIN_EMAIL
|
||||
ARG NODE_ENV
|
||||
COPY package.json ./
|
||||
RUN npm install --also=dev
|
||||
COPY . ./
|
||||
RUN npm run build
|
||||
RUN env
|
||||
|
||||
FROM node:8.11
|
||||
|
||||
|
||||
ENV HOME=/home/app
|
||||
|
||||
RUN mkdir -p /home/app
|
||||
# production environment
|
||||
FROM node:13.12.0-alpine
|
||||
RUN mkdir -p /app/public
|
||||
COPY ./package.json /tmp
|
||||
RUN cd /tmp && npm install
|
||||
RUN cp -a /tmp/node_modules /home/app
|
||||
|
||||
# add `/usr/src/app/node_modules/.bin` to $PATH
|
||||
ENV PATH /usr/app/node_modules/.bin:$PATH
|
||||
|
||||
WORKDIR $HOME
|
||||
COPY . $HOME/
|
||||
RUN npm run build
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
RUN cp -a /tmp/node_modules /app
|
||||
RUN env
|
||||
WORKDIR /app
|
||||
COPY server /app
|
||||
COPY --from=build /app/build /app/public
|
||||
CMD ["node", "index.js"]
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
Called during development only. Allows us to use ES6 and JSX in our server code.
|
||||
*/
|
||||
|
||||
require('@babel/register')({
|
||||
presets: ["@babel/preset-env",
|
||||
"@babel/react"]
|
||||
});
|
||||
|
||||
require("./src/server/index")
|
||||
10
account/notes.md
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
## Base
|
||||
start using `npx create-react-app account --template redux`
|
||||
|
||||
## Packages used:
|
||||
- [connected-react-router](https://github.com/supasate/connected-react-router): Synchronize router state with redux store through
|
||||
- [Redux Thunk](https://github.com/reduxjs/redux-thunk): lets you do AJAX calls
|
||||
- [axios](https://github.com/axios/axios) - library for doing HTTP requests
|
||||
- [react-router-dom](https://reactrouter.com/web/guides/quick-start) - Adds routes to React
|
||||
- [Semantic UI](https://react.semantic-ui.com/usage) - The UI frontend
|
||||
30600
account/package-lock.json
generated
@@ -1,90 +1,53 @@
|
||||
{
|
||||
"name": "react-passport-redux-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Demonstration of PassportJS authentication in React w/ Redux.",
|
||||
"main": "devStart.js",
|
||||
"scripts": {
|
||||
"build:lib": "babel src/server --out-dir lib/server",
|
||||
"build:umd": "webpack --config webpack/webpack.config.dev.js",
|
||||
"build:umd:min": "webpack --config webpack/webpack.config.prod.js",
|
||||
"build": "npm run clean && npm run build:lib && npm run build:umd && npm run build:umd:min",
|
||||
"postinstall": "npm run build",
|
||||
"clean": "rimraf lib public/assets",
|
||||
"server": "better-npm-run server",
|
||||
"server:dev": "better-npm-run server:dev",
|
||||
"start": "npm run server",
|
||||
"start:dev": "npm run server:dev",
|
||||
"storybook": "start-storybook -p 9001 -c .storybook"
|
||||
},
|
||||
"betterScripts": {
|
||||
"server": {
|
||||
"command": "node lib/server/index.js",
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"MIN_EXT": ".min"
|
||||
}
|
||||
},
|
||||
"server:dev": {
|
||||
"command": "node devStart.js",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.3.1",
|
||||
"axios": "0.18.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"better-npm-run": "0.1.1",
|
||||
"body-parser": "1.18.3",
|
||||
"connect-mongo": "2.0.3",
|
||||
"connected-react-router": "6.3.1",
|
||||
"csv-parse": "4.3.3",
|
||||
"d3-time-format": "2.1.3",
|
||||
"express": "4.16.4",
|
||||
"express-session": "1.15.6",
|
||||
"fs-extra": "7.0.1",
|
||||
"lodash": "4.17.11",
|
||||
"mongoose": "5.4.15",
|
||||
"multer": "1.4.1",
|
||||
"node-mailjet": "3.3.1",
|
||||
"node-uuid": "1.4.8",
|
||||
"passport": "0.4.0",
|
||||
"passport-local": "1.0.0",
|
||||
"react": "16.8.3",
|
||||
"react-dom": "16.8.3",
|
||||
"react-redux": "6.0.1",
|
||||
"react-router": "4.3.1",
|
||||
"react-router-dom": "4.3.1",
|
||||
"react-router-redux": "5.0.0-alpha.9",
|
||||
"react-stripe-elements": "2.0.3",
|
||||
"redux": "4.0.1",
|
||||
"name": "account",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@reduxjs/toolkit": "^1.1.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"axios": "0.21.1",
|
||||
"connected-react-router": "6.8.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-scripts": "4.0.1",
|
||||
"redux-thunk": "2.3.0",
|
||||
"semantic-ui-css": "2.4.1",
|
||||
"semantic-ui-react": "0.85.0",
|
||||
"storybook": "1.0.0",
|
||||
"stripe": "6.25.1"
|
||||
"semantic-ui-react": "2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.2.3",
|
||||
"@babel/core": "7.3.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.3.0",
|
||||
"@babel/plugin-proposal-export-default-from": "7.2.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.3.1",
|
||||
"@babel/plugin-transform-runtime": "7.2.0",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-stage-1": "^7.0.0",
|
||||
"@babel/register": "^7.0.0",
|
||||
"@storybook/react": "4.1.13",
|
||||
"babel-loader": "8.0.5",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"detect-node": "^2.0.4",
|
||||
"redux-logger": "3.0.6",
|
||||
"rimraf": "2.6.3",
|
||||
"webpack": "4.29.5",
|
||||
"webpack-cli": "3.2.3",
|
||||
"webpack-dev-middleware": "^3.5.1",
|
||||
"webpack-hot-middleware": "^2.24.3"
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "5.0.0",
|
||||
"connect-mongo": "3.2.0",
|
||||
"express": "4.17.1",
|
||||
"express-session": "1.17.1",
|
||||
"mongoose": "5.11.10",
|
||||
"node-mailjet": "3.3.1",
|
||||
"passport": "0.4.1",
|
||||
"passport-local": "1.0.0",
|
||||
"passport-twitter": "1.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
account/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
account/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
account/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
account/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 403 B |
BIN
account/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 627 B |
BIN
account/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
43
account/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="%REACT_APP_SITE_NAME%"
|
||||
content="Listen to live Police and Fire radio broadcasts. Archived for 30 days and searchable."
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>%REACT_APP_SITE_NAME%</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
2
account/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
1
account/public/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
92
account/server/config/express.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const path = require("path");
|
||||
const express = require("express");
|
||||
const session = require("express-session");
|
||||
const bodyParser = require("body-parser");
|
||||
const connectMongo = require("connect-mongo");
|
||||
const secrets = require("./secrets");
|
||||
|
||||
const MongoStore = connectMongo(session)
|
||||
var cookie_domain = process.env['REACT_APP_COOKIE_DOMAIN'] != null ? process.env['REACT_APP_COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var backend_server = process.env['REACT_APP_BACKEND_SERVER'] != null ? process.env['REACT_APP_BACKEND_SERVER'] : 'https://api.openmhz.com';
|
||||
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||
var socket_server = process.env['REACT_APP_SOCKET_SERVER'] != null ? process.env['REACT_APP_SOCKET_SERVER'] : 'wss://socket.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : 'https://account.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var dev_server = frontend_server + ":3000"
|
||||
|
||||
module.exports = function(app, passport) {
|
||||
app.set("port", 3009)
|
||||
|
||||
// X-Powered-By header has no functional value.
|
||||
// Keeping it makes it easier for an attacker to build the site's profile
|
||||
// It can be removed safely
|
||||
app.disable("x-powered-by")
|
||||
app.enable('trust proxy')
|
||||
app.use(bodyParser.json())
|
||||
app.use(bodyParser.urlencoded({ extended: true }))
|
||||
app.use(express.static(path.join(process.cwd(), 'public')));
|
||||
|
||||
const sess = {
|
||||
/*saveUninitialized: false,
|
||||
resave: false,*/
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
secret: secrets.sessionSecret,
|
||||
proxy: true,
|
||||
name: "sessionId",
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
domain: cookie_domain
|
||||
},
|
||||
store: new MongoStore({
|
||||
url: secrets.db,
|
||||
autoReconnect: true
|
||||
})
|
||||
}
|
||||
|
||||
var node_env = process.env.NODE_ENV;
|
||||
console.log('--------------------------');
|
||||
console.log('===> 😊 Starting Server . . .');
|
||||
console.log('===> Environment: ' + node_env);
|
||||
if(node_env === 'production') {
|
||||
console.log('===> 🚦 Note: In order for authentication to work in production');
|
||||
console.log('===> you will need a secure HTTPS connection');
|
||||
sess.cookie.secure = true; // Serve secure cookies
|
||||
sess.cookie.httpOnly = false;
|
||||
}
|
||||
|
||||
app.use(session(sess))
|
||||
|
||||
app.use(passport.initialize())
|
||||
app.use(passport.session())
|
||||
|
||||
app.use('/*', function(req, res, next) {
|
||||
var allowedOrigins = [ account_server];
|
||||
allowedOrigins.push(frontend_server);
|
||||
allowedOrigins.push(backend_server);
|
||||
allowedOrigins.push(socket_server);
|
||||
allowedOrigins.push(dev_server);
|
||||
var origin = req.headers.origin;
|
||||
|
||||
|
||||
if (allowedOrigins.indexOf(origin) > -1) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
} else if (req.headers["user-agent"] == 'TrunkRecorder1.0') {
|
||||
res.setHeader('Access-Control-Allow-Origin', "*");
|
||||
} else {
|
||||
res.setHeader('Access-Control-Allow-Origin', "*");
|
||||
if (origin) {
|
||||
console.warn("forcing CORS for: " + origin + " referer: " + req.headers.referer);
|
||||
}
|
||||
}
|
||||
res.header("Access-Control-Allow-Headers", "X-Requested-With");
|
||||
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS,WEBSOCKET');
|
||||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
res.header("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Max-Age");
|
||||
res.header('Access-Control-Max-Age', '600');
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -3,15 +3,13 @@
|
||||
Code modified from : https://github.com/madhums/node-express-mongoose-demo/blob/master/config/passport/local.js
|
||||
*/
|
||||
|
||||
import mongoose from "mongoose"
|
||||
import { Strategy as LocalStrategy } from "passport-local"
|
||||
import User from "../../models/user"
|
||||
|
||||
const LocalStrategy = require("passport-local").Strategy;
|
||||
const User = require("../../models/user");
|
||||
/*
|
||||
By default, LocalStrategy expects to find credentials in parameters named username and password.
|
||||
If your site prefers to name these fields differently, options are available to change the defaults.
|
||||
*/
|
||||
export default new LocalStrategy({
|
||||
const local = new LocalStrategy({
|
||||
usernameField: "email"
|
||||
}, (email, password, done) => {
|
||||
User.findOne({
|
||||
@@ -23,13 +21,18 @@
|
||||
}
|
||||
}]
|
||||
}, (err, user) => {
|
||||
if(!user) return done(null, false, { message: `Email ${email} not found` })
|
||||
if(!user) {
|
||||
console.error("Auth Error - user not found: " + email);
|
||||
return done(null, false, { message: `Invalid email or password` })
|
||||
}
|
||||
user.comparePassword(password, (err, isMatch) => {
|
||||
if (isMatch) {
|
||||
return done(null, user)
|
||||
} else {
|
||||
console.error("Auth Error - password mismatch: " + email);
|
||||
return done(null, false, { message: "Invalid email or password" })
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
module.exports = local;
|
||||
18
account/server/config/passport-strategies/twitter.js
Normal file
@@ -0,0 +1,18 @@
|
||||
var Strategy = require('passport-twitter').Strategy;
|
||||
const User = require("../../models/user");
|
||||
|
||||
const twitter = new Strategy({
|
||||
consumerKey: process.env['TWITTER_CONSUMER_KEY'],
|
||||
consumerSecret: process.env['TWITTER_CONSUMER_SECRET'],
|
||||
callbackURL: '/oauth/callback'
|
||||
},
|
||||
function(token, tokenSecret, profile, cb) {
|
||||
// In this example, the user's Twitter profile is supplied as the user
|
||||
// record. In a production-quality application, the Twitter profile should
|
||||
// be associated with a user record in the application's database, which
|
||||
// allows for account linking and authentication with other identity
|
||||
// providers.
|
||||
return cb(null, profile);
|
||||
})
|
||||
|
||||
module.exports = local;
|
||||
@@ -1,8 +1,8 @@
|
||||
/* Initializing PassportJS */
|
||||
import User from "../models/user"
|
||||
import local from "./passport-strategies/local"
|
||||
const User = require("../models/user");
|
||||
const local = require("./passport-strategies/local");
|
||||
|
||||
export default function (app, passport) {
|
||||
module.exports = function (app, passport) {
|
||||
// Configure Passport authenticated session persistence.
|
||||
//
|
||||
// In order to restore authentication state across HTTP requests, Passport needs
|
||||
@@ -23,3 +23,4 @@ export default function (app, passport) {
|
||||
// use the following strategies
|
||||
passport.use(local)
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ const secrets = {
|
||||
sessionSecret: "letthisbeyoursecret"
|
||||
}
|
||||
|
||||
export default secrets
|
||||
module.exports = secrets
|
||||
@@ -1,5 +1,5 @@
|
||||
import mongoose from "mongoose";
|
||||
import System from "../models/system";
|
||||
const mongoose = require("mongoose");
|
||||
const System = require("../models/system");
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import mongoose from "mongoose";
|
||||
import passport from "passport";
|
||||
import User from "../models/user";
|
||||
import secrets from "../config/secrets";
|
||||
const passport = require("passport");
|
||||
const User = require("../models/user");
|
||||
|
||||
const mailjet = require("node-mailjet").connect(
|
||||
process.env['MAILJET_KEY'],
|
||||
process.env['MAILJET_SECRET']
|
||||
);
|
||||
const crypto = require("crypto");
|
||||
|
||||
var admin_email = process.env['ADMIN_EMAIL'] != null ? process.env['ADMIN_EMAIL'] : "luke@openmhz.com";
|
||||
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
|
||||
var account_server = process.env['ACCOUNT_SERVER'] != null ? process.env['ACCOUNT_SERVER'] : "https://account.openmhz.com";
|
||||
var cookie_domain = process.env['COOKIE_DOMAIN'] != null ? process.env['COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var admin_email = process.env['REACT_APP_ADMIN_EMAIL'] != null ? process.env['REACT_APP_ADMIN_EMAIL'] : "luke@openmhz.com";
|
||||
var site_name = process.env['REACT_APP_SITE_NAME'] != null ? process.env['REACT_APP_SITE_NAME'] : "OpenMHz";
|
||||
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : "https://account.openmhz.com";
|
||||
var cookie_domain = process.env['REACT_APP_COOKIE_DOMAIN'] != null ? process.env['REACT_APP_COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
|
||||
exports.isLoggedIn = function(req, res, next) {
|
||||
if (req.isAuthenticated()) return next();
|
||||
@@ -22,8 +21,7 @@ exports.isLoggedIn = function(req, res, next) {
|
||||
|
||||
exports.authenticated = function(req, res, next) {
|
||||
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
|
||||
console.log("auth: " + req.isAuthenticated());
|
||||
console.log("req.user: " + req.user);
|
||||
console.log("account/server/controllers/users.js - exports.authenticated() Auth check for user: " + req.user + " is: " + req.isAuthenticated());
|
||||
if (req.isAuthenticated()) {
|
||||
var clientUser = (({
|
||||
firstName,
|
||||
@@ -43,11 +41,7 @@ exports.authenticated = function(req, res, next) {
|
||||
req.user
|
||||
);
|
||||
clientUser.userId = req.user.id
|
||||
if (req.user.customerId) {
|
||||
clientUser.billing = true;
|
||||
} else {
|
||||
clientUser.billing = false;
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
user: clientUser
|
||||
@@ -94,7 +88,7 @@ exports.login = function(req, res, next) {
|
||||
userId: user.id
|
||||
});
|
||||
}
|
||||
console.log(user);
|
||||
//console.log("account/server/controllers/users.js - req.login() Authenicated: " + user.email);
|
||||
// go ahead and create the new user
|
||||
var clientUser = (({
|
||||
firstName,
|
||||
@@ -455,6 +449,7 @@ exports.terms = function(req, res, next) {
|
||||
|
||||
|
||||
exports.validateProfile = function(req, res, next) {
|
||||
console.log("Validating user profile: "+ req.body.email);
|
||||
if (!req.body.firstName || (req.body.firstName.length < 2)) {
|
||||
console.error("ERROR: Validate System - req.body.firstName");
|
||||
res.json({
|
||||
@@ -515,7 +510,7 @@ exports.updateProfile = function(req, res, next) {
|
||||
user.screenName = res.locals.screenName
|
||||
user.location = res.locals.location
|
||||
|
||||
console.log(user)
|
||||
|
||||
user.save(err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
@@ -550,6 +545,8 @@ exports.updateProfile = function(req, res, next) {
|
||||
// -------------------------------------------
|
||||
|
||||
exports.register = function(req, res, next) {
|
||||
console.log("Registration request for: " + req.body.email);
|
||||
|
||||
User.findOne({
|
||||
$or: [{
|
||||
email: req.body.email
|
||||
@@ -586,16 +583,16 @@ exports.register = function(req, res, next) {
|
||||
);
|
||||
user.password = req.body.password;
|
||||
user.email = req.body.email;
|
||||
|
||||
|
||||
User.create(user).then(function(dbUser) {
|
||||
console.log(dbUser);
|
||||
console.log("Successfully registered: " + dbUser.email + ", now sending confirmation email.");
|
||||
// Since regiestration worked, send a confirmation email.
|
||||
return handleSendConfirmEmail(dbUser);
|
||||
}).then(function(result) {
|
||||
res.json(result);
|
||||
return;
|
||||
}).catch(function(err) {
|
||||
console.error(err);
|
||||
console.error("Error creating user: " + user.email + " Error: " +err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
112
account/server/index.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const fs = require("fs");
|
||||
const express = require("express");
|
||||
const mongoose = require("mongoose");
|
||||
const passport = require("passport");
|
||||
const path = require("path");
|
||||
const secrets = require("./config/secrets");
|
||||
const configurePassport = require("./config/passport");
|
||||
const configureExpress = require("./config/express");
|
||||
const users = require("./controllers/users");
|
||||
require("./models/user");
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
const app = express()
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
async function connect() {
|
||||
|
||||
// Demonstrate the readyState and on event emitters
|
||||
console.log(mongoose.connection.readyState); //logs 0
|
||||
mongoose.connection.on('connecting', () => {
|
||||
console.log('Mongoose is connecting')
|
||||
console.log(mongoose.connection.readyState); //logs 2
|
||||
});
|
||||
mongoose.connection.on('connected', () => {
|
||||
console.log('Mongoose is connected');
|
||||
console.log(mongoose.connection.readyState); //logs 1
|
||||
});
|
||||
mongoose.connection.on('disconnecting', () => {
|
||||
console.log('Mongoose is disconnecting');
|
||||
console.log(mongoose.connection.readyState); // logs 3
|
||||
});
|
||||
|
||||
// Connect to a MongoDB server running on 'localhost:27017' and use the
|
||||
// 'test' database.
|
||||
await mongoose.connect(secrets.db, { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true }, (err, res) => {
|
||||
if (err) {
|
||||
console.log(`Mongoose - Error connecting to ${secrets.db}. ${err}`)
|
||||
} else {
|
||||
console.log(`Mongoose Successfully connected to ${secrets.db}.`)
|
||||
}
|
||||
})
|
||||
console.log("All Done");
|
||||
}
|
||||
connect();
|
||||
mongoose.connection.on('error', err => {
|
||||
console.error("Mongoose Error!")
|
||||
console.error(err);
|
||||
});
|
||||
mongoose.connection.on('disconnected', () => {
|
||||
console.log('Mongoose disconnected');
|
||||
console.log(mongoose.connection.readyState); //logs 0
|
||||
connect();
|
||||
});
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development"
|
||||
|
||||
|
||||
|
||||
|
||||
var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||
var account_server = process.env['REACT_APP_ACCOUNT_SERVER'] != null ? process.env['REACT_APP_ACCOUNT_SERVER'] : 'https://account.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var admin_server = process.env['REACT_APP_ADMIN_SERVER'] != null ? process.env['REACT_APP_ADMIN_SERVER'] : 'https://admin.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
configurePassport(app, passport)
|
||||
configureExpress(app, passport)
|
||||
|
||||
|
||||
app.all('*', function(req, res, next) {
|
||||
var allowedOrigins = [account_server, admin_server, frontend_server];
|
||||
var origin = req.headers.origin;
|
||||
if(allowedOrigins.indexOf(origin) > -1){
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
res.header("Access-Control-Allow-Credentials", "true");
|
||||
res.header("Access-Control-Allow-Headers", "X-Requested-With");
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type');
|
||||
next();
|
||||
});
|
||||
|
||||
// -------------------------------------------
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
app.post("/login", users.login)
|
||||
app.get("/logout", users.logout)
|
||||
app.get("/authenticated", users.authenticated)
|
||||
app.post("/register", users.validateProfile, users.register)
|
||||
app.post("/users/:userId/reset-password/:token", users.resetPassword)
|
||||
app.post("/api/send-reset-password", users.sendResetPassword)
|
||||
app.post("/users/:userId", users.isLoggedIn, users.validateProfile, users.updateProfile)
|
||||
app.post("/users/:userId/terms", users.isLoggedIn, users.terms)
|
||||
app.post("/users/:userId/send-confirm", users.sendConfirmEmail)
|
||||
app.post("/users/:userId/confirm/:token", users.confirmEmail)
|
||||
|
||||
app.get("*", (req, res, next) => {
|
||||
res.sendFile(__dirname + '/public/index.html');
|
||||
});
|
||||
|
||||
|
||||
// start listening to incoming requests
|
||||
app.listen(app.get("port"), app.get("host"), (err) => {
|
||||
if (err) {
|
||||
console.err(err.stack)
|
||||
} else {
|
||||
console.log(`App listening on port ${app.get("port")} [${process.env.NODE_ENV} mode]`)
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
var mongoose = require('mongoose');
|
||||
var bcrypt = require('bcrypt-nodejs');
|
||||
require('mongoose');
|
||||
require('bcrypt-nodejs');
|
||||
|
||||
var systemSchema = mongoose.Schema({
|
||||
name: String,
|
||||
@@ -1,8 +1,8 @@
|
||||
// Defining a User Model in mongoose
|
||||
// Code modified from https://github.com/sahat/hackathon-starter
|
||||
import bcrypt from "bcrypt-nodejs"
|
||||
import mongoose from "mongoose"
|
||||
import crypto from "crypto"
|
||||
const bcrypt = require("bcrypt");
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -33,10 +33,6 @@ const UserSchema = new mongoose.Schema({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customerId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
confirmEmailToken: String,
|
||||
confirmEmailTTL: Date,
|
||||
admin: {
|
||||
@@ -58,6 +54,7 @@ const UserSchema = new mongoose.Schema({
|
||||
/**
|
||||
* Password hash middleware.
|
||||
*/
|
||||
/*
|
||||
UserSchema.pre("save", function(next) {
|
||||
var user = this
|
||||
if (!user.isModified("password")) return next()
|
||||
@@ -70,11 +67,23 @@ UserSchema.pre("save", function(next) {
|
||||
next()
|
||||
})
|
||||
})
|
||||
})
|
||||
})*/
|
||||
|
||||
UserSchema.pre("save", function(next) {
|
||||
if(!this.isModified("password")) {
|
||||
return next();
|
||||
}
|
||||
hashed = bcrypt.hashSync(this.password, 8);
|
||||
this.password = hashed;
|
||||
this.local.password = hashed;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
/*
|
||||
Defining our own custom document instance method
|
||||
*/
|
||||
/*
|
||||
UserSchema.methods = {
|
||||
comparePassword: function(candidatePassword, cb) {
|
||||
bcrypt.compare(candidatePassword, this.local.password, (err, isMatch) => {
|
||||
@@ -82,11 +91,16 @@ UserSchema.pre("save", function(next) {
|
||||
cb(null, isMatch)
|
||||
})
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
UserSchema.methods.comparePassword = function(plaintext, callback) {
|
||||
return callback(null, bcrypt.compareSync(plaintext, this.local.password));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Statics
|
||||
*/
|
||||
UserSchema.statics = {}
|
||||
|
||||
export default mongoose.model("User", UserSchema)
|
||||
module.exports = mongoose.model("User", UserSchema)
|
||||
@@ -2,10 +2,6 @@ import React, { Component } from "react"
|
||||
import NavigationContainer from "./NavigationContainer"
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div>
|
||||
@@ -1,13 +1,10 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Menu, Dropdown } from "semantic-ui-react";
|
||||
|
||||
const navStyle = {
|
||||
marginBottom: "30px"
|
||||
};
|
||||
const blankStyle = {
|
||||
marginBottom: "50px"
|
||||
};
|
||||
|
||||
class Navigation extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -20,14 +17,13 @@ class Navigation extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
var profileLink = account_server + "/profile";
|
||||
var billingLink = account_server + "/billing";
|
||||
var profileLink = process.env.REACT_APP_ACCOUNT_SERVER + "/profile";
|
||||
return (
|
||||
<div>
|
||||
{this.props.user.authenticated ? (
|
||||
<Menu style={navStyle}>
|
||||
<Menu.Item name="systems" header>
|
||||
OpenMHz
|
||||
{process.env.REACT_APP_SITE_NAME}
|
||||
</Menu.Item>
|
||||
<Menu.Menu position="right">
|
||||
{/* <Link to="/update-plans">
|
||||
@@ -1,6 +1,6 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as userActions from "../../User/user-actions"
|
||||
import Navigation from "../pure/Navigation"
|
||||
import * as userActions from "../User/user-actions"
|
||||
import Navigation from "./Navigation"
|
||||
|
||||
|
||||
// Function passed in to `connect` to subscribe to Redux store updates.
|
||||
@@ -1,13 +1,12 @@
|
||||
// in src/restricted.js
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import * as userActions from "../../User/user-actions"
|
||||
import * as types from "../../User/user-constants";
|
||||
import * as types from "../User/user-constants";
|
||||
import axios from "axios";
|
||||
/**
|
||||
* Higher-order component (HOC) to wrap restricted pages
|
||||
*/
|
||||
export default (BaseComponent, store) => {
|
||||
const restricted = (BaseComponent, store) => {
|
||||
class Restricted extends Component {
|
||||
componentWillMount() {
|
||||
this.checkAuthentication(this.props);
|
||||
@@ -53,3 +52,4 @@ export default (BaseComponent, store) => {
|
||||
}
|
||||
return withRouter(Restricted);
|
||||
}
|
||||
export default restricted
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Dimmer,
|
||||
Loader,
|
||||
Button,
|
||||
@@ -11,9 +10,7 @@ import {
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
class ConfirmEmail extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
confirmMessage: { success: false, message:""}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { PropTypes, Component } from "react";
|
||||
import React, { Component } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import {
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
@@ -47,20 +46,21 @@ class Login extends Component {
|
||||
_onLoginSubmit(event) {
|
||||
event.preventDefault();
|
||||
const { email, password } = this.state;
|
||||
var success = {type: "path", nextPathname: "/" };
|
||||
var success = { type: "path", nextPathname: "/" };
|
||||
if (this.props.nextPathname) {
|
||||
success.nextPathname = this.props.nextPathname;
|
||||
}
|
||||
if (this.props.nextLocation) {
|
||||
success.type = "location";
|
||||
switch(this.props.nextLocation) {
|
||||
success.type = "location";
|
||||
switch (this.props.nextLocation) {
|
||||
case "frontend":
|
||||
success.nextLocation = process.env.REACT_APP_FRONTEND_SERVER;
|
||||
break;
|
||||
default:
|
||||
case "admin":
|
||||
success.nextLocation = admin_server;
|
||||
break;
|
||||
case "frontend":
|
||||
success.nextLocation = frontend_server;
|
||||
break;
|
||||
}
|
||||
success.nextLocation = process.env.REACT_APP_ADMIN_SERVER;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Passed in via react-redux. Returns a promise.
|
||||
@@ -86,7 +86,6 @@ class Login extends Component {
|
||||
|
||||
render() {
|
||||
var loginMessage = "";
|
||||
const { name, email } = this.state;
|
||||
|
||||
if (this.state.loginMessage) {
|
||||
loginMessage = (
|
||||
@@ -102,46 +101,46 @@ class Login extends Component {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Header as="h1">{site_name}</Header>
|
||||
<Header as="h1">{process.env.REACT_APP_SITE_NAME}</Header>
|
||||
<Grid stackable >
|
||||
<Grid.Row>
|
||||
<Grid.Column width={9}>
|
||||
|
||||
<Header size="large">Let's get this started!</Header>
|
||||
<Header size="small">
|
||||
{site_name} makes it easy to share recordings from{" "}
|
||||
<a href="https://github.com/robotastic/trunk-recorder">
|
||||
Trunk Recorder
|
||||
<Header size="large">Let's get this started!</Header>
|
||||
<Header size="small">
|
||||
{process.env.REACT_APP_SITE_NAME} makes it easy to share recordings from{" "}
|
||||
<a href="https://github.com/robotastic/trunk-recorder">
|
||||
Trunk Recorder
|
||||
</a>.</Header>
|
||||
<p>
|
||||
Trunk Recorder makes it easy to record the radio systems
|
||||
used by your local police and fire department. Trunk Recorder
|
||||
is sort of like an awesome version of those Scanners from
|
||||
Radio Shack... 10-4!
|
||||
<p>
|
||||
Trunk Recorder makes it easy to record the radio systems
|
||||
used by your local police and fire department. Trunk Recorder
|
||||
is sort of like an awesome version of those Scanners from
|
||||
Radio Shack... 10-4!
|
||||
</p>
|
||||
<p>
|
||||
This site is a work in progress. I plan on adding more
|
||||
features over time, but right now it can:
|
||||
<p>
|
||||
This site is a work in progress. I plan on adding more
|
||||
features over time, but right now it can:
|
||||
</p>
|
||||
<ul>
|
||||
<li>3 day archive</li>
|
||||
<li>Filter by talkgroup</li>
|
||||
</ul>
|
||||
<p>
|
||||
All of this goodness is free! In the future, I might offer
|
||||
paid accounts with additional features and a longer archive
|
||||
period, but I will always try to have a free version.
|
||||
<ul>
|
||||
<li>30 day archive</li>
|
||||
<li>Filter by talkgroup</li>
|
||||
</ul>
|
||||
<p>
|
||||
All of this goodness is free! In the future, I might offer
|
||||
paid accounts with additional features and a longer archive
|
||||
period, but I will always try to have a free version.
|
||||
</p>
|
||||
<p>
|
||||
{" "}
|
||||
<p>
|
||||
{" "}
|
||||
- Luke{" "}
|
||||
<a
|
||||
href="mailto:{admin_email}?Subject={site_name}"
|
||||
target="_top"
|
||||
>
|
||||
admin@email.com
|
||||
<a
|
||||
href="mailto:{process.env.REACT_APP_ADMIN_EMAIL}?Subject={process.env.REACT_APP_SITE_NAME}"
|
||||
target="_top"
|
||||
>
|
||||
admin@email.com
|
||||
</a>{" "}
|
||||
</p>
|
||||
</p>
|
||||
</Grid.Column>
|
||||
<Grid.Column floated="right" width={6}>
|
||||
<Header as="h3">Need an account?</Header>
|
||||
@@ -151,8 +150,8 @@ class Login extends Component {
|
||||
onClick={this.handleRegisterClick}
|
||||
fluid
|
||||
/>
|
||||
<Divider style={dividerStyle} horizontal>Or</Divider>
|
||||
<Header as="h3">Login</Header>
|
||||
<Divider style={dividerStyle} horizontal>Or</Divider>
|
||||
<Header as="h3">Login</Header>
|
||||
<Form onSubmit={this._onLoginSubmit}>
|
||||
<Segment padded>
|
||||
<Form.Field>
|
||||
@@ -183,7 +182,7 @@ class Login extends Component {
|
||||
</Segment>
|
||||
</Form>
|
||||
<div style={forgotStyle} >
|
||||
<Link to="/send-reset-password">Forgot Password</Link>
|
||||
<Link to="/send-reset-password">Forgot Password</Link>
|
||||
</div>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
@@ -1,5 +1,4 @@
|
||||
import { connect } from "react-redux"
|
||||
import { push } from 'react-router-redux'
|
||||
import * as userActions from "./user-actions"
|
||||
import Login from "./Login"
|
||||
import queryString from '../query-string';
|
||||
@@ -1,24 +1,13 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon
|
||||
} from "semantic-ui-react";
|
||||
|
||||
import UserForm from "./UserForm";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const requestMessageStyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
class Register extends Component {
|
||||
constructor(props) {
|
||||
@@ -1,14 +1,9 @@
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon
|
||||
} from "semantic-ui-react";
|
||||
|
||||
class ResetPassword extends Component {
|
||||
@@ -75,7 +70,6 @@ class ResetPassword extends Component {
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="password"
|
||||
type="password"
|
||||
onChange={this.handleInputChange}
|
||||
@@ -86,7 +80,6 @@ class ResetPassword extends Component {
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
onChange={this.handleInputChange}
|
||||
@@ -1,11 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon
|
||||
28
account/src/User/SentConfirmEmail.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { Component } from "react"
|
||||
import {
|
||||
Container,
|
||||
Message,
|
||||
Icon
|
||||
} from "semantic-ui-react";
|
||||
|
||||
class SentConfirmEmail extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Container text>
|
||||
<Message icon>
|
||||
<Icon name='envelope outline' />
|
||||
<Message.Content>
|
||||
<Message.Header>Confirmation email sent!</Message.Header>
|
||||
<p>We sent an email to the address you gave us: {this.props.user.email}<br />
|
||||
Please click on the link in the email to verify the email address.</p>
|
||||
</Message.Content>
|
||||
</Message>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SentConfirmEmail
|
||||
84
account/src/User/Terms.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Segment,
|
||||
Container,
|
||||
Button
|
||||
} from "semantic-ui-react";
|
||||
|
||||
class Terms extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleAccept = this.handleAccept.bind(this);
|
||||
this.handleDecline = this.handleDecline.bind(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
handleAccept() {
|
||||
this.props
|
||||
.acceptTerms(this.props.user.userId)
|
||||
.then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({
|
||||
requestMessage
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleDecline() {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
<Container text>
|
||||
<Segment attached>
|
||||
The agreement is useful to The Guardian to set the rules and guidelines for visitors, users and customers:
|
||||
|
||||
Registration: inform users that they must agree to a set of rules when they register on your website or mobile app
|
||||
Disable or terminate accounts: inform users that their accounts can be disabled or terminated if abuses happen on your website or mobile app, e.g. spamming, swearing etc.
|
||||
Owner of content: notify users that you, the company, is the owner of the content appearing on your website or mobile app – except in cases where other users can post content (upload, share, create etc.) where the users are the owners of such content
|
||||
Changes to the agreement: users should be informed about upcoming changes to the agreement before the changes are applied.
|
||||
DMCA: The Guardian is also informing owners of copyright content that they can submit a DMCA notice to the website if they found infringing content in their DMCA clause.
|
||||
And more
|
||||
These are only a few examples of what kind of clauses and disclosures you can add in this kind of agreement.
|
||||
|
||||
What is a Terms of Use
|
||||
While a Terms of Use is recommended to have, it’s not mandatory by law as the Privacy Policy agreement is required.
|
||||
|
||||
This agreement serves a single purpose: to inform users of rules that they must agree to. A Privacy Policy must inform users about your privacy practices (what data you collect, why, how, etc.)
|
||||
|
||||
The definition of a Terms of Use agreement is simple:
|
||||
|
||||
Terms of service (also known as terms of use and terms and conditions, commonly abbreviated as ToS or TOS and TOU) are rules which one must agree to abide by in order to use a service.
|
||||
|
||||
Wikipedia
|
||||
Because this agreement simply acts as a contract between you, the company, and the users using or accessing your website or mobile app, the agreement can be named as you’d like:
|
||||
|
||||
Terms of Use (ToU)
|
||||
Terms of Service (ToS)
|
||||
Terms and Conditions (T&C)
|
||||
User Agreement
|
||||
Conditions of Use
|
||||
And so on
|
||||
The “Conditions of Use” of Amazon could be named “Terms of Use”:
|
||||
</Segment>
|
||||
<Button.Group attached="bottom">
|
||||
<Button
|
||||
onClick={this.handleDecline}
|
||||
>Decline</Button>
|
||||
<Button.Or />
|
||||
<Button positive
|
||||
onClick={this.handleAccept}
|
||||
>Accept</Button>
|
||||
</Button.Group>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Terms;
|
||||
@@ -1,23 +1,13 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Dimmer,
|
||||
Loader,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const checkInputMessagestyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
class UserForm extends Component {
|
||||
@@ -60,7 +50,7 @@ class UserForm extends Component {
|
||||
|
||||
checkInputs() {
|
||||
let error = false;
|
||||
var checkInputMessages= [];
|
||||
var checkInputMessages = [];
|
||||
|
||||
if (this.state.firstName === "") {
|
||||
this.setState({ firstNameError: true });
|
||||
@@ -115,7 +105,7 @@ class UserForm extends Component {
|
||||
this.setState({ passwordError: true });
|
||||
checkInputMessages.push("Password must be 7 charecters or more");
|
||||
error = true;
|
||||
}else {
|
||||
} else {
|
||||
this.setState({ passwordError: false });
|
||||
}
|
||||
|
||||
@@ -138,7 +128,7 @@ class UserForm extends Component {
|
||||
let inputError = this.checkInputs();
|
||||
|
||||
if (!inputError) {
|
||||
const user = (({ firstName, lastName, screenName, location, email, password }) => ({ firstName, lastName, screenName, location, email, password }))(this.state);
|
||||
const user = (({ firstName, lastName, screenName, location, email, password }) => ({ firstName, lastName, screenName, location, email, password }))(this.state);
|
||||
this.props.onSubmit(user);
|
||||
}
|
||||
}
|
||||
@@ -148,12 +138,11 @@ class UserForm extends Component {
|
||||
var floatStyle = {
|
||||
overflow: 'auto'
|
||||
}
|
||||
var dimmerProps = { };
|
||||
var dimmerProps = {};
|
||||
if (this.props.isWaiting) {
|
||||
dimmerProps["active"] = true;
|
||||
}
|
||||
const { name, email } = this.state;
|
||||
const { isEditing} = this.props;
|
||||
const { isEditing } = this.props;
|
||||
if (this.state.checkInputMessages.length || this.props.requestMessage.length) {
|
||||
var counter = 0;
|
||||
registerMessage = (
|
||||
@@ -162,11 +151,11 @@ class UserForm extends Component {
|
||||
<Message.Content>
|
||||
<Message.Header>Problems...</Message.Header>
|
||||
<ul>
|
||||
{this.state.checkInputMessages.map(function(listValue) {
|
||||
{this.state.checkInputMessages.map(function (listValue) {
|
||||
return <li key={`ListItem_${counter++}`}>{listValue}</li>;
|
||||
})}
|
||||
{ this.props.requestMessage.length>0 &&
|
||||
<li key="req-mess">{this.props.requestMessage}</li>
|
||||
{this.props.requestMessage.length > 0 &&
|
||||
<li key="req-mess">{this.props.requestMessage}</li>
|
||||
}
|
||||
</ul>
|
||||
</Message.Content>
|
||||
@@ -175,85 +164,84 @@ class UserForm extends Component {
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
className="raised padding segment"
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
<Form
|
||||
className="raised padding segment"
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
<Dimmer {...dimmerProps}>
|
||||
<Loader indeterminate>Updating Profile</Loader>
|
||||
</Dimmer>
|
||||
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="firstName"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.firstNameError}
|
||||
defaultValue={this.state.firstName}
|
||||
label="First name"
|
||||
placeholder="First Name..."
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="lastName"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.lastNameError}
|
||||
defaultValue={this.state.lastName}
|
||||
label="Last name"
|
||||
placeholder="Last Name..."
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="email"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.emailError}
|
||||
defaultValue={this.state.email}
|
||||
disabled={isEditing}
|
||||
label="Email"
|
||||
placeholder="Email..."
|
||||
/>
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="firstName"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.firstNameError}
|
||||
defaultValue={this.state.firstName}
|
||||
label="First name"
|
||||
placeholder="First Name..."
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="lastName"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.lastNameError}
|
||||
defaultValue={this.state.lastName}
|
||||
label="Last name"
|
||||
placeholder="Last Name..."
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="email"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.emailError}
|
||||
defaultValue={this.state.email}
|
||||
disabled={isEditing}
|
||||
label="Email"
|
||||
placeholder="Email..."
|
||||
/>
|
||||
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="screenName"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.screenNameError}
|
||||
defaultValue={this.state.screenName}
|
||||
label="Screen Name"
|
||||
placeholder="Screen Name..."
|
||||
/>
|
||||
<p style={{ fontStyle: "italic" }}>
|
||||
Shown on OpenMHz for each of your systems
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="screenName"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.screenNameError}
|
||||
defaultValue={this.state.screenName}
|
||||
label="Screen Name"
|
||||
placeholder="Screen Name..."
|
||||
/>
|
||||
<p style={{ fontStyle: "italic" }}>
|
||||
Shown on OpenMHz for each of your systems
|
||||
</p>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="location"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.locationError}
|
||||
defaultValue={this.state.location}
|
||||
label="General Location"
|
||||
placeholder="City, State..."
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
{isEditing !== true && (
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="location"
|
||||
onChange={this.handleInputChange}
|
||||
error={this.state.locationError}
|
||||
defaultValue={this.state.location}
|
||||
label="General Location"
|
||||
placeholder="City, State..."
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
{isEditing!=true && (
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="password"
|
||||
type="password"
|
||||
onChange={this.handleInputChange}
|
||||
@@ -265,7 +253,6 @@ class UserForm extends Component {
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Input
|
||||
type="text"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
onChange={this.handleInputChange}
|
||||
@@ -276,14 +263,14 @@ class UserForm extends Component {
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
)}
|
||||
<div style={floatStyle}>
|
||||
<Button type="submit" size="large" floated="right" value="Login" color='blue'>
|
||||
{isEditing!=true? 'Register' : 'Update' }
|
||||
)}
|
||||
<div style={floatStyle}>
|
||||
<Button type="submit" size="large" floated="right" value="Login" color='blue'>
|
||||
{isEditing !== true ? 'Register' : 'Update'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{registerMessage}
|
||||
</Form>
|
||||
{registerMessage}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { Component } from "react"
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
@@ -156,7 +156,7 @@ export function manualLogin(
|
||||
return dispatch => {
|
||||
dispatch(beginLogin());
|
||||
|
||||
return makeUserRequest("post", data, "/login", {withCredentials: true})
|
||||
return makeUserRequest("post", data, process.env.REACT_APP_ACCOUNT_SERVER + "/login", { withCredentials: true })
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
const user = response.data.user
|
||||
@@ -164,17 +164,17 @@ export function manualLogin(
|
||||
// use browserHistory singleton to control navigation. Will generate a
|
||||
// state change for time-traveling as we are using the react-router-redux package
|
||||
//browserHistory.push(successPath)
|
||||
if (success && (success.type=="path")) {
|
||||
if (success && (success.type === "path")) {
|
||||
dispatch(push(success.nextPathname));
|
||||
} else if (success && (success.type=="location")) {
|
||||
window.location=success.nextLocation;
|
||||
} else if (success && (success.type === "location")) {
|
||||
window.location = success.nextLocation;
|
||||
} else {
|
||||
dispatch(push("/"));
|
||||
}
|
||||
} else {
|
||||
let loginMessage = response.data.message;
|
||||
console.log("loginMessage");
|
||||
if (loginMessage == "unconfirmed email") {
|
||||
if (loginMessage === "unconfirmed email") {
|
||||
data.userId = response.data.userId;
|
||||
dispatch(loginEmailError(data));
|
||||
dispatch(push("/wait-confirm-email"));
|
||||
@@ -184,7 +184,7 @@ export function manualLogin(
|
||||
return loginMessage;
|
||||
}
|
||||
})
|
||||
.catch(function(response) {
|
||||
.catch(function (response) {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
@@ -197,7 +197,6 @@ export function manualLogin(
|
||||
// http://redux.js.org/docs/advanced/AsyncActions.html
|
||||
export function isAuthenticated() {
|
||||
return dispatch => {
|
||||
dispatch(beginAuthenticated());
|
||||
|
||||
return axios
|
||||
.get("/authenticated")
|
||||
@@ -253,15 +252,15 @@ export function manualLogout() {
|
||||
export function confirmEmail(userId, token) {
|
||||
return dispatch => {
|
||||
dispatch(beginConfirm());
|
||||
return makeUserRequest("post", {}, "/users/" + userId + "/confirm/" + token).then(
|
||||
return makeUserRequest("post", {}, process.env.REACT_APP_ACCOUNT_SERVER + "/users/" + userId + "/confirm/" + token).then(
|
||||
response => {
|
||||
if (response.data.success) {
|
||||
dispatch(confirmSuccess());
|
||||
let requestMessage = {success: true, message: response.data.message};
|
||||
let requestMessage = { success: true, message: response.data.message };
|
||||
return requestMessage;
|
||||
} else {
|
||||
dispatch(confirmError());
|
||||
let requestMessage = {success: false, message: response.data.message};
|
||||
let requestMessage = { success: false, message: response.data.message };
|
||||
return requestMessage;
|
||||
}
|
||||
}
|
||||
@@ -269,10 +268,10 @@ export function confirmEmail(userId, token) {
|
||||
};
|
||||
}
|
||||
|
||||
export function resetPassword(userId, token,password) {
|
||||
export function resetPassword(userId, token, password) {
|
||||
return dispatch => {
|
||||
dispatch(beginResetPassword());
|
||||
return makeUserRequest("post", {password: password}, "/users/" + userId + "/reset-password/" + token).then(
|
||||
return makeUserRequest("post", { password: password }, process.env.REACT_APP_ACCOUNT_SERVER + "/users/" + userId + "/reset-password/" + token).then(
|
||||
response => {
|
||||
if (response.data.success) {
|
||||
dispatch(resetPasswordSuccess());
|
||||
@@ -290,7 +289,7 @@ export function resetPassword(userId, token,password) {
|
||||
export function sendResetPassword(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginSendResetPassword());
|
||||
return makeUserRequest("post", data, "/api/send-reset-password").then(
|
||||
return makeUserRequest("post", data, process.env.REACT_APP_ACCOUNT_SERVER + "/api/send-reset-password").then(
|
||||
response => {
|
||||
if (response.data.success) {
|
||||
dispatch(sendResetPasswordSuccess());
|
||||
@@ -308,7 +307,7 @@ export function sendResetPassword(data) {
|
||||
export function sendConfirmEmail(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginSendConfirm());
|
||||
return makeUserRequest("post", data, "/users/" + data + "/send-confirm").then(
|
||||
return makeUserRequest("post", data, process.env.REACT_APP_ACCOUNT_SERVER + "/users/" + data + "/send-confirm").then(
|
||||
response => {
|
||||
if (response.data.success) {
|
||||
dispatch(sendConfirmSuccess());
|
||||
@@ -326,7 +325,7 @@ export function sendConfirmEmail(data) {
|
||||
export function acceptTerms(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginAgreeTerms());
|
||||
return makeUserRequest("post", data, "/users/" + data + "/terms").then(
|
||||
return makeUserRequest("post", data, process.env.REACT_APP_ACCOUNT_SERVER + "/users/" + data + "/terms").then(
|
||||
response => {
|
||||
if (response.data.success) {
|
||||
dispatch(agreeTermsSuccess());
|
||||
@@ -344,7 +343,7 @@ export function updateProfile(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginUpdateProfile());
|
||||
|
||||
return makeUserRequest("post", data, "/users/" + data.userId)
|
||||
return makeUserRequest("post", data, process.env.REACT_APP_ACCOUNT_SERVER + "/users/" + data.userId)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
const user = response.data.user
|
||||
@@ -368,8 +367,8 @@ export function updateProfile(data) {
|
||||
export function manualRegister(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginRegister());
|
||||
|
||||
return makeUserRequest("post", data, "/register")
|
||||
console.log("It is: " + process.env.REACT_APP_ACCOUNT_SERVER );
|
||||
return makeUserRequest("post", data, process.env.REACT_APP_ACCOUNT_SERVER + "/register")
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
data.userId = response.data.userId;
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import { Elements, StripeProvider} from 'react-stripe-elements';
|
||||
import { Container, Header, Segment, Icon, Divider, Button, Message } from 'semantic-ui-react'
|
||||
import PaymentSource from "./PaymentSource"
|
||||
import InvoiceList from "./InvoiceList"
|
||||
// ----------------------------------------------------
|
||||
class Billing extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleShowAddSource = this.handleShowAddSource.bind(this);
|
||||
this.handleNewSource = this.handleNewSource.bind(this);
|
||||
this.state = {showAddSource: false};
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchBilling();
|
||||
this.props.fetchInvoices();
|
||||
}
|
||||
handleShowAddSource() {
|
||||
this.setState({
|
||||
showAddSource: !this.state.showAddSource
|
||||
});
|
||||
}
|
||||
handleNewSource(newSource) {
|
||||
this.setState({
|
||||
showAddSource: false
|
||||
});
|
||||
this.props.updateCard(newSource);
|
||||
}
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
var name = this.props.user.firstName + " " + this.props.user.lastName;
|
||||
var section=<div/>;
|
||||
if (!this.props.billing || this.state.showAddSource) {
|
||||
section = (<div>
|
||||
<StripeProvider apiKey="">
|
||||
<Elements>
|
||||
<PaymentSource name={name} email={this.props.user.email} onCancel={this.handleShowAddSource} onSubmit={this.handleNewSource}/>
|
||||
</Elements>
|
||||
</StripeProvider>
|
||||
</div>)
|
||||
} else {
|
||||
section = (<Segment padded='very'>
|
||||
<Divider horizontal>
|
||||
<Header as='h3'>
|
||||
<Icon name='credit card outline' />
|
||||
Card on File
|
||||
</Header>
|
||||
</Divider>
|
||||
<Header as='h3'>
|
||||
<Header.Content>
|
||||
{this.props.billing.source.name} xxxx-{this.props.billing.source.last4}
|
||||
<Header.Subheader>Expires {this.props.billing.source.exp_month} / {this.props.billing.source.exp_year}</Header.Subheader>
|
||||
</Header.Content>
|
||||
</Header>
|
||||
<Button onClick={this.handleShowAddSource}>Change Card</Button>
|
||||
</Segment>)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container text>
|
||||
<Header as='h2'>
|
||||
Payment Method</Header>
|
||||
<p>It is free to use OpenMHz. Add payment in order to upgrade features.</p>
|
||||
{section}
|
||||
<Header as='h2'>
|
||||
Payment History</Header>
|
||||
{this.props.billing.invoices.length > 0 ? (
|
||||
<InvoiceList invoices={this.props.billing.invoices}/>
|
||||
) : (
|
||||
<Message>
|
||||
<Message.Header>Changes in Service</Message.Header>
|
||||
<p>
|
||||
We updated our privacy policy here to better service our customers. We recommend reviewing the
|
||||
changes.
|
||||
</p>
|
||||
</Message>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Billing;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as billingActions from "./billing-actions"
|
||||
import Billing from "./Billing"
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
billing: state.billing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
billingActions
|
||||
)(Billing)
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import {
|
||||
Container,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
|
||||
class InvoiceList extends Component {
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const DATE_OPTIONS = { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' };
|
||||
return (
|
||||
<Table singleLine>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Date</Table.HeaderCell>
|
||||
<Table.HeaderCell>Amount</Table.HeaderCell>
|
||||
<Table.HeaderCell>Status</Table.HeaderCell>
|
||||
<Table.HeaderCell></Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{this.props.invoices.map((invoice, index) => {
|
||||
return( <Table.Row>
|
||||
<Table.Cell>{(new Date(invoice.date*1000)).toLocaleDateString('en-US', DATE_OPTIONS)}</Table.Cell>
|
||||
<Table.Cell>${invoice.amount_due/100}</Table.Cell>
|
||||
<Table.Cell>{invoice.status} </Table.Cell>
|
||||
<Table.Cell><a href={invoice.invoice_pdf}>View Invoice</a> </Table.Cell>
|
||||
</Table.Row>)
|
||||
} )}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default InvoiceList;
|
||||
@@ -1,52 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import { Header, Button, Segment, Form, Icon, Divider } from 'semantic-ui-react'
|
||||
import {CardElement, injectStripe} from 'react-stripe-elements';
|
||||
|
||||
class PaymentSource extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.submit = this.submit.bind(this);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.state = {name: this.props.name ? this.props.name : ""};
|
||||
}
|
||||
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
|
||||
async submit(ev) {
|
||||
console.log("here");
|
||||
this.props.stripe.createSource({type: 'card', owner: {
|
||||
name: this.state.name, email: this.props.email
|
||||
}}).then(({source}) => {
|
||||
this.props.onSubmit({sourceId: source.id })
|
||||
console.log('Received Stripe token:', source);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="checkout">
|
||||
<Segment padded='very'>
|
||||
<Form>
|
||||
<Divider horizontal>
|
||||
<Header as='h3'>
|
||||
<Icon name='credit card outline' />
|
||||
Credit or Debit Card
|
||||
</Header>
|
||||
</Divider>
|
||||
<Form.Input fluid label='Name on card' placeholder='Name'
|
||||
name="name"
|
||||
defaultValue={this.state.name}
|
||||
onChange={this.handleInputChange}/>
|
||||
<Form.Field>
|
||||
<CardElement />
|
||||
</Form.Field>
|
||||
<Divider hidden />
|
||||
<Button onClick={this.submit}>Submit</Button>
|
||||
</Form>
|
||||
<img src="../../resources/powered_by_stripe.png" />
|
||||
</Segment>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectStripe(PaymentSource);
|
||||
@@ -1,145 +0,0 @@
|
||||
import axios from "axios";
|
||||
import * as types from "./billing-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
|
||||
function beginUpdateCard() {
|
||||
return { type: types.UPDATE_CARD };
|
||||
}
|
||||
|
||||
function updateCardSuccess(data) {
|
||||
return { type: types.UPDATE_CARD_SUCCESS, data };
|
||||
}
|
||||
|
||||
function updateCardError(data) {
|
||||
return { type: types.UPDATE_CARD_ERROR, data };
|
||||
}
|
||||
|
||||
|
||||
function beginFetchBilling() {
|
||||
return { type: types.FETCH_BILLING };
|
||||
}
|
||||
|
||||
function fetchBillingSuccess(data) {
|
||||
return { type: types.FETCH_BILLING_SUCCESS,
|
||||
data };
|
||||
}
|
||||
|
||||
|
||||
function fetchBillingError(data) {
|
||||
return { type: types.FETCH_BILLING_ERROR,
|
||||
data };
|
||||
}
|
||||
|
||||
function beginFetchInvoice() {
|
||||
return { type: types.FETCH_INVOICE };
|
||||
}
|
||||
|
||||
function fetchInvoiceSuccess(data) {
|
||||
return { type: types.FETCH_INVOICE_SUCCESS,
|
||||
data };
|
||||
}
|
||||
|
||||
|
||||
function fetchInvoiceError(data) {
|
||||
return { type: types.FETCH_INVOICE_ERROR,
|
||||
data };
|
||||
}
|
||||
|
||||
function makeUserRequest(method, data, api = "/login") {
|
||||
// returns a Promise
|
||||
return axios({
|
||||
method: method,
|
||||
url: api,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
export function changeUrl(url) {
|
||||
return dispatch => {
|
||||
dispatch(push(url));
|
||||
};
|
||||
}
|
||||
|
||||
// Example of an Async Action Creator
|
||||
// http://redux.js.org/docs/advanced/AsyncActions.html
|
||||
|
||||
export function updateCard(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginUpdateCard());
|
||||
|
||||
return makeUserRequest("post", data, "/api/customer/", {withCredentials: true})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
|
||||
|
||||
dispatch(updateCardSuccess(response.data));
|
||||
} else {
|
||||
dispatch(updateCardError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export function fetchBilling() {
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch(beginFetchBilling());
|
||||
|
||||
return axios
|
||||
.get("/api/billing", {withCredentials: true})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(fetchBillingSuccess(response.data));
|
||||
} else {
|
||||
dispatch(fetchBillingError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchInvoices() {
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch(beginFetchInvoice());
|
||||
|
||||
return axios
|
||||
.get("/api/invoices", {withCredentials: true})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(fetchInvoiceSuccess(response.data));
|
||||
} else {
|
||||
dispatch(fetchInvoiceError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
export const UPDATE_CARD = "UPDATE_CARD"
|
||||
export const UPDATE_CARD_SUCCESS = "UPDATE_CARD_SUCCESS"
|
||||
export const UPDATE_CARD_ERROR = "UPDATE_CARD_ERROR"
|
||||
export const FETCH_BILLING = "FETCH_BILLING"
|
||||
export const FETCH_BILLING_SUCCESS = "FETCH_BILLING_SUCCESS"
|
||||
export const FETCH_BILLING_ERROR = "FETCH_BILLING_ERROR"
|
||||
export const FETCH_INVOICE = "FETCH_INVOICE"
|
||||
export const FETCH_INVOICE_SUCCESS = "FETCH_INVOICE_SUCCESS"
|
||||
export const FETCH_INVOICE_ERROR = "FETCH_INVOICE_ERROR"
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import {
|
||||
UPDATE_CARD,
|
||||
UPDATE_CARD_SUCCESS,
|
||||
UPDATE_CARD_ERROR,
|
||||
FETCH_BILLING,
|
||||
FETCH_BILLING_SUCCESS,
|
||||
FETCH_BILLING_ERROR,
|
||||
FETCH_INVOICE,
|
||||
FETCH_INVOICE_SUCCESS,
|
||||
FETCH_INVOICE_ERROR
|
||||
} from "./billing-constants";
|
||||
|
||||
function updateObject(oldObject, newValues) {
|
||||
// Encapsulate the idea of passing a new object as the first parameter
|
||||
// to Object.assign to ensure we correctly copy data instead of mutating
|
||||
return Object.assign({}, oldObject, newValues);
|
||||
}
|
||||
|
||||
function updateObjectInArray(array, shortName, updatedItem) {
|
||||
return array.map( item => {
|
||||
if(item.shortName !== shortName) {
|
||||
// This isn't the item we care about - keep it as-is
|
||||
return item;
|
||||
}
|
||||
|
||||
// Otherwise, this is the one we want - return an updated value
|
||||
return {
|
||||
...item,
|
||||
...updatedItem
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const billing = (
|
||||
state = {
|
||||
isWaiting: false,
|
||||
source: false,
|
||||
invoices: []},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
case UPDATE_CARD:
|
||||
case FETCH_BILLING:
|
||||
case FETCH_INVOICE:
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
|
||||
case UPDATE_CARD_SUCCESS:
|
||||
return Object.assign({}, state, { source: action.data.source, isWaiting: false});
|
||||
|
||||
case FETCH_BILLING_SUCCESS:
|
||||
return Object.assign({}, state, { source: action.data.source, isWaiting: false});
|
||||
|
||||
case FETCH_INVOICE_SUCCESS:
|
||||
return Object.assign({}, state, { invoices: action.data.invoices, isWaiting: false});
|
||||
|
||||
case UPDATE_CARD_ERROR:
|
||||
case FETCH_BILLING_ERROR:
|
||||
case FETCH_INVOICE_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default billing;
|
||||
@@ -1,140 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import {
|
||||
Container,
|
||||
Divider,
|
||||
Checkbox,
|
||||
Button,
|
||||
Header,
|
||||
Table, Form
|
||||
} from "semantic-ui-react";
|
||||
// ----------------------------------------------------
|
||||
|
||||
|
||||
function planPrice(planType) {
|
||||
switch(planType) {
|
||||
case freePlanValue:
|
||||
return freePlanPrice;
|
||||
case proPlanValue:
|
||||
return proPlanPrice;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ListPlans extends Component {
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
this.handleUpdate = this.handleUpdate.bind(this);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
var plans = {};
|
||||
var prices = {};
|
||||
var total = 0;
|
||||
|
||||
this.props.systems.forEach(system => {
|
||||
plans[system.shortName] = system.planType;
|
||||
prices[system.shortName] = planPrice(system.planType);
|
||||
total += planPrice(system.planType);
|
||||
});
|
||||
this.state = { plans: plans, prices: prices, total: total };
|
||||
}
|
||||
|
||||
|
||||
handleCancel() {
|
||||
var obj = {}
|
||||
this.props.systems.forEach(system => {
|
||||
obj[system.shortName] = system.planType;
|
||||
});
|
||||
this.setState({ plans: obj });
|
||||
}
|
||||
handleUpdate() {
|
||||
this.props.onUpdate(this.state.plans);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
}
|
||||
|
||||
handleInputChange(e, { name, value }) {
|
||||
var plans = this.state.plans;
|
||||
var prices = this.state.prices;
|
||||
var total = 0;
|
||||
plans[name] = value;
|
||||
prices[name] = planPrice(value);
|
||||
for (var system in prices) {
|
||||
total += prices[system];
|
||||
}
|
||||
this.setState({ plans: plans, prices: prices, total: total });
|
||||
}
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table definition>
|
||||
<Table.Header>
|
||||
|
||||
<Table.Row>
|
||||
<Table.HeaderCell />
|
||||
<Table.HeaderCell textAlign='center'>Free</Table.HeaderCell>
|
||||
<Table.HeaderCell textAlign='center'>Pro</Table.HeaderCell>
|
||||
<Table.HeaderCell textAlign='center'>Monthly Price</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<tbody>
|
||||
{this.props.systems.map((system) =>
|
||||
|
||||
<Table.Row key={system.shortName}>
|
||||
<Table.Cell>
|
||||
<Header as='h4'>
|
||||
<Header.Content>
|
||||
{system.name}
|
||||
<Header.Subheader>{system.shortName}</Header.Subheader>
|
||||
</Header.Content>
|
||||
|
||||
</Header>
|
||||
</Table.Cell>
|
||||
<Table.Cell textAlign='center'>
|
||||
<Checkbox
|
||||
radio
|
||||
value={0}
|
||||
name={system.shortName}
|
||||
checked={this.state.plans[system.shortName] === 0}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell textAlign='center'>
|
||||
<Checkbox
|
||||
radio
|
||||
value={10}
|
||||
name={system.shortName}
|
||||
checked={this.state.plans[system.shortName] === 10}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell textAlign='center'>${this.state.prices[system.shortName]}</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
)}
|
||||
<Table.Row key="total">
|
||||
<Table.Cell></Table.Cell>
|
||||
<Table.Cell></Table.Cell>
|
||||
<Table.Cell textAlign='right'>Estimated Monthly Total:</Table.Cell>
|
||||
<Table.Cell textAlign='center'>${this.state.total}</Table.Cell>
|
||||
</Table.Row>
|
||||
</tbody>
|
||||
</Table>
|
||||
<Button.Group floated="right">
|
||||
<Button content='Cancel' onClick={this.handleCancel} />
|
||||
<Button content='Update' onClick={this.handleUpdate} primary />
|
||||
</Button.Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ListPlans;
|
||||
@@ -1,131 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Container,
|
||||
Divider,
|
||||
Modal,
|
||||
Button,
|
||||
Segment,
|
||||
Icon,
|
||||
Link,
|
||||
Header, Form, List} from "semantic-ui-react";
|
||||
import ListPlans from "./ListPlans"
|
||||
|
||||
|
||||
function planName(planType) {
|
||||
switch(planType) {
|
||||
case freePlanValue:
|
||||
return "Free";
|
||||
case proPlanValue:
|
||||
return "Pro";
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
class UpdatePlans extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleUpdate = this.handleUpdate.bind(this);
|
||||
this.handleOk = this.handleOk.bind(this);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
this.findPlanChanges = this.findPlanChanges.bind(this);
|
||||
}
|
||||
|
||||
state = { modalOpen: false, updatedPlans: {}, changes: [] }
|
||||
|
||||
showModal = () => this.setState({ modalOpen: true })
|
||||
closeModal = () => this.setState({ modalOpen: false })
|
||||
|
||||
findPlanChanges(plans) {
|
||||
var changes = [];
|
||||
const systems = this.props.system.items;
|
||||
systems.forEach(system => {
|
||||
const newPlan = plans[system.shortName];
|
||||
if (newPlan != system.planType) {
|
||||
changes.push( system.name + " - " + planName(newPlan));
|
||||
}
|
||||
});
|
||||
return changes;
|
||||
}
|
||||
|
||||
|
||||
handleOk(){
|
||||
this.props.updatePlan(this.state.updatedPlans);
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
handleCancel() {
|
||||
this.setState({updatedPlans: {}});
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
handleUpdate(plans) {
|
||||
const changes = this.findPlanChanges(plans);
|
||||
|
||||
if (Array.isArray(changes) && changes.length) {
|
||||
var result = Object.keys(plans).map(function(key) {
|
||||
return {shortName: key, planType: plans[key]};
|
||||
});
|
||||
this.setState({updatedPlans: result, changes: changes});
|
||||
this.showModal()
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchSystems();
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
const systems = this.props.system.items;
|
||||
const billingDefined = this.props.user.billing;
|
||||
return (
|
||||
<div>
|
||||
<Container text>
|
||||
<Modal size="tiny" open={this.state.modalOpen} onClose={this.closeModal}>
|
||||
<Modal.Header>Update Plans</Modal.Header>
|
||||
<Modal.Content>
|
||||
|
||||
<Header as='h4'>The following changes will be made:</Header>
|
||||
<List as='ul'>
|
||||
{this.state.changes.map((change) =>
|
||||
<List.Item as='li' content={change}/>
|
||||
)}
|
||||
</List>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={this.handleCancel} >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={this.handleOk} primary>
|
||||
OK
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
<Form>
|
||||
<Header as="h1">System Plans</Header>
|
||||
|
||||
<Divider />
|
||||
{billingDefined? (
|
||||
<ListPlans systems={systems} onUpdate={this.handleUpdate} key={systems.length}/>
|
||||
) : (
|
||||
<Segment>
|
||||
<Header icon>
|
||||
<Icon name='credit card outline' />
|
||||
You need to add a Payment Method before you can upgrade a Plan
|
||||
</Header>
|
||||
<Button primary
|
||||
to='/billing'>Add Payment Method</Button>
|
||||
</Segment>
|
||||
)}
|
||||
</Form>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default UpdatePlans;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as systemActions from "../System/system-actions"
|
||||
import * as billingActions from "../Billing/billing-actions"
|
||||
import UpdatePlans from "./UpdatePlans"
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
system: state.system,
|
||||
user: state.user
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
systemActions
|
||||
)(UpdatePlans)
|
||||
@@ -1,156 +0,0 @@
|
||||
import axios from "axios";
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./system-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
|
||||
function beginUpdateSystem() {
|
||||
return { type: types.UPDATE_SYSTEM };
|
||||
}
|
||||
|
||||
function updateSystemSuccess(data) {
|
||||
return { type: types.UPDATE_SUCCESS_SYSTEM, data };
|
||||
}
|
||||
|
||||
function updateSystemError(data) {
|
||||
return { type: types.UPDATE_ERROR_SYSTEM, data };
|
||||
}
|
||||
|
||||
|
||||
function beginFetchSystem() {
|
||||
return { type: types.FETCH_SYSTEM };
|
||||
}
|
||||
|
||||
function fetchSystemSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_SYSTEM,
|
||||
data };
|
||||
}
|
||||
|
||||
|
||||
function fetchSystemError(data) {
|
||||
return { type: types.FETCH_ERROR_SYSTEM,
|
||||
data };
|
||||
}
|
||||
|
||||
function beginUpdatePlan() {
|
||||
return { type: types.UPDATE_PLAN };
|
||||
}
|
||||
|
||||
function updatePlanSuccess(data) {
|
||||
return { type: types.UPDATE_PLAN_SUCCESS,
|
||||
data };
|
||||
}
|
||||
|
||||
|
||||
function updatePlanError(data) {
|
||||
return { type: types.UPDATE_PLAN_ERROR,
|
||||
data };
|
||||
}
|
||||
|
||||
|
||||
function makeUserRequest(method, data, api = "/login") {
|
||||
// returns a Promise
|
||||
return axios({
|
||||
method: method,
|
||||
url: api,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
export function changeUrl(url) {
|
||||
return dispatch => {
|
||||
dispatch(push(url));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function updatePlan(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginUpdatePlan());
|
||||
|
||||
return makeUserRequest("post", {updatePlans:data}, account_server + "/api/plans/", {withCredentials: true})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(updatePlanSuccess(response.data.systems));
|
||||
} else {
|
||||
dispatch(updatePlanError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Example of an Async Action Creator
|
||||
// http://redux.js.org/docs/advanced/AsyncActions.html
|
||||
|
||||
export function updateSystem(data) {
|
||||
return dispatch => {
|
||||
dispatch(beginUpdateSystem());
|
||||
|
||||
return makeUserRequest("post", data, admin_server + "/systems/" + data.shortName, {withCredentials: true})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
const user = response.data.user
|
||||
|
||||
dispatch(updateSystemSuccess(response.data.system));
|
||||
dispatch(push("/system/"+data.shortName));
|
||||
} else {
|
||||
dispatch(updateProfileError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function shouldFetchSystems(state, subreddit) {
|
||||
if ((!state.system.items) || (state.system.items.length <1)) {
|
||||
return true
|
||||
} else if (state.system.isWaiting) {
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function fetchSystems() {
|
||||
return (dispatch, getState) => {
|
||||
if (shouldFetchSystems(getState())) {
|
||||
dispatch(beginFetchSystem());
|
||||
|
||||
return axios
|
||||
.get(admin_server + "/systems", {withCredentials: true})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(fetchSystemSuccess(response.data.systems));
|
||||
} else {
|
||||
dispatch(fetchSystemError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
export const UPDATE_SYSTEM = "UPDATE_SYSTEM"
|
||||
export const UPDATE_SUCCESS_SYSTEM = "UPDATE_SUCCESS_SYSTEM"
|
||||
export const UPDATE_ERROR_SYSTEM = "UPDATE_ERROR_SYSTEM"
|
||||
export const FETCH_SYSTEM = "FETCH_SYSTEM"
|
||||
export const FETCH_SUCCESS_SYSTEM = "FETCH_SUCCESS_SYSTEM"
|
||||
export const FETCH_ERROR_SYSTEM = "FETCH_ERROR_SYSTEM"
|
||||
export const UPDATE_PLAN = "UPDATE_PLAN"
|
||||
export const UPDATE_PLAN_SUCCESS = "UPDATE_PLAN_SUCCESS"
|
||||
export const UPDATE_PLAN_ERROR = "UPDATE_PLAN_ERROR"
|
||||
@@ -1,78 +0,0 @@
|
||||
import {
|
||||
UPDATE_SYSTEM,
|
||||
UPDATE_SUCCESS_SYSTEM,
|
||||
UPDATE_ERROR_SYSTEM,
|
||||
FETCH_SYSTEM,
|
||||
FETCH_SUCCESS_SYSTEM,
|
||||
FETCH_ERROR_SYSTEM,
|
||||
UPDATE_PLAN,
|
||||
UPDATE_PLAN_SUCCESS,
|
||||
UPDATE_PLAN_ERROR
|
||||
} from "./system-constants";
|
||||
|
||||
function updateObject(oldObject, newValues) {
|
||||
// Encapsulate the idea of passing a new object as the first parameter
|
||||
// to Object.assign to ensure we correctly copy data instead of mutating
|
||||
return Object.assign({}, oldObject, newValues);
|
||||
}
|
||||
|
||||
function updateObjectInArray(array, shortName, updatedItem) {
|
||||
return array.map( item => {
|
||||
if(item.shortName !== shortName) {
|
||||
// This isn't the item we care about - keep it as-is
|
||||
return item;
|
||||
}
|
||||
|
||||
// Otherwise, this is the one we want - return an updated value
|
||||
return {
|
||||
...item,
|
||||
...updatedItem
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const system = (
|
||||
state = {
|
||||
isWaiting: false,
|
||||
items: []},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
case UPDATE_SYSTEM:
|
||||
case FETCH_SYSTEM:
|
||||
case UPDATE_PLAN:
|
||||
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
|
||||
case UPDATE_PLAN_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: action.data
|
||||
});
|
||||
|
||||
case UPDATE_SUCCESS_SYSTEM:
|
||||
return Object.assign({}, state, {
|
||||
items: updateObjectInArray(state.items, action.data.shortName, action.data)
|
||||
});
|
||||
case FETCH_SUCCESS_SYSTEM:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: action.data
|
||||
});
|
||||
|
||||
case UPDATE_PLAN_ERROR:
|
||||
case UPDATE_ERROR_SYSTEM:
|
||||
case FETCH_ERROR_SYSTEM:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default system;
|
||||
@@ -1,36 +0,0 @@
|
||||
import React, { Component } from "react"
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon
|
||||
} from "semantic-ui-react";
|
||||
|
||||
class SentConfirmEmail extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
render() {
|
||||
return(
|
||||
<div>
|
||||
<Container text>
|
||||
<Message icon>
|
||||
<Icon name='envelope outline'/>
|
||||
<Message.Content>
|
||||
<Message.Header>Confirmation email sent!</Message.Header>
|
||||
<p>We sent an email to the address you gave us: {this.props.user.email}<br/>
|
||||
Please click on the link in the email to verify the email address.</p>
|
||||
</Message.Content>
|
||||
</Message>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SentConfirmEmail
|
||||
@@ -1,83 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import { Segment,
|
||||
TextArea,
|
||||
Container,
|
||||
Button } from "semantic-ui-react";
|
||||
|
||||
class Terms extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleAccept = this.handleAccept.bind(this);
|
||||
this.handleDecline = this.handleDecline.bind(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
handleAccept() {
|
||||
this.props
|
||||
.acceptTerms(this.props.user.userId)
|
||||
.then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({
|
||||
requestMessage
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleDecline() {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
<Container text>
|
||||
<Segment attached>
|
||||
The agreement is useful to The Guardian to set the rules and guidelines for visitors, users and customers:
|
||||
|
||||
Registration: inform users that they must agree to a set of rules when they register on your website or mobile app
|
||||
Disable or terminate accounts: inform users that their accounts can be disabled or terminated if abuses happen on your website or mobile app, e.g. spamming, swearing etc.
|
||||
Owner of content: notify users that you, the company, is the owner of the content appearing on your website or mobile app – except in cases where other users can post content (upload, share, create etc.) where the users are the owners of such content
|
||||
Changes to the agreement: users should be informed about upcoming changes to the agreement before the changes are applied.
|
||||
DMCA: The Guardian is also informing owners of copyright content that they can submit a DMCA notice to the website if they found infringing content in their DMCA clause.
|
||||
And more
|
||||
These are only a few examples of what kind of clauses and disclosures you can add in this kind of agreement.
|
||||
|
||||
What is a Terms of Use
|
||||
While a Terms of Use is recommended to have, it’s not mandatory by law as the Privacy Policy agreement is required.
|
||||
|
||||
This agreement serves a single purpose: to inform users of rules that they must agree to. A Privacy Policy must inform users about your privacy practices (what data you collect, why, how, etc.)
|
||||
|
||||
The definition of a Terms of Use agreement is simple:
|
||||
|
||||
Terms of service (also known as terms of use and terms and conditions, commonly abbreviated as ToS or TOS and TOU) are rules which one must agree to abide by in order to use a service.
|
||||
|
||||
Wikipedia
|
||||
Because this agreement simply acts as a contract between you, the company, and the users using or accessing your website or mobile app, the agreement can be named as you’d like:
|
||||
|
||||
Terms of Use (ToU)
|
||||
Terms of Service (ToS)
|
||||
Terms and Conditions (T&C)
|
||||
User Agreement
|
||||
Conditions of Use
|
||||
And so on
|
||||
The “Conditions of Use” of Amazon could be named “Terms of Use”:
|
||||
</Segment>
|
||||
<Button.Group attached="bottom">
|
||||
<Button
|
||||
onClick={this.handleDecline}
|
||||
>Decline</Button>
|
||||
<Button.Or />
|
||||
<Button positive
|
||||
onClick={this.handleAccept}
|
||||
>Accept</Button>
|
||||
</Button.Group>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Terms;
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from "react"
|
||||
import { render } from "react-dom"
|
||||
import { Provider } from "react-redux"
|
||||
import { Switch, Route} from 'react-router-dom'
|
||||
import { createStore, combineReducers, applyMiddleware } from 'redux'
|
||||
import thunk from "redux-thunk"
|
||||
import {createLogger} from "redux-logger"
|
||||
//import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux'
|
||||
import { routerMiddleware } from 'connected-react-router'
|
||||
import { ConnectedRouter } from 'connected-react-router'
|
||||
import isNode from "detect-node"
|
||||
//import rootReducer from "./reducers"
|
||||
import createRootReducer from './reducers'
|
||||
import { createHashHistory } from 'history'
|
||||
import createHistory from 'history/createBrowserHistory'
|
||||
|
||||
import App from "./components/container/App"
|
||||
import LoginContainer from "./User/LoginContainer"
|
||||
import RegisterContainer from "./User/RegisterContainer"
|
||||
import ProfileContainer from "./User/ProfileContainer"
|
||||
import BillingContainer from "./Billing/BillingContainer"
|
||||
import Default from "./components/pure/Default"
|
||||
import NavigationContainer from "./components/container/NavigationContainer"
|
||||
import ConfirmEmailContainer from "./User/ConfirmEmailContainer"
|
||||
import WaitConfirmEmailContainer from "./User/WaitConfirmEmailContainer"
|
||||
import SentConfirmEmailContainer from "./User/SentConfirmEmailContainer"
|
||||
import SendResetPasswordContainer from "./User/SendResetPasswordContainer"
|
||||
import ResetPasswordContainer from "./User/ResetPasswordContainer"
|
||||
import Restricted from "./components/container/Restricted"
|
||||
import TermsContainer from "./User/TermsContainer"
|
||||
import UpdatePlans from "./Plan/UpdatePlansContainer"
|
||||
|
||||
// Create a history of your choosing (we're using a browser history in this case)
|
||||
const history = createHistory()
|
||||
|
||||
// Build the middleware for intercepting and dispatching navigation actions
|
||||
const historyMiddleware = routerMiddleware(history)
|
||||
|
||||
let middleware = [
|
||||
thunk,
|
||||
historyMiddleware
|
||||
|
||||
]
|
||||
|
||||
// only add Redux-Logger on the client-side because it causes problems server-side.
|
||||
if (!isNode) {
|
||||
middleware.push(createLogger())
|
||||
}
|
||||
|
||||
|
||||
// Add the reducer to your store on the `router` key
|
||||
// Also apply our middleware for navigating
|
||||
const store = createStore(
|
||||
createRootReducer(history),
|
||||
applyMiddleware(thunk,
|
||||
historyMiddleware, createLogger())
|
||||
)
|
||||
|
||||
/*
|
||||
<Route path="/system/:shortName" component={Restricted(SystemContainer, store)} />
|
||||
<Route path="/update-system/:shortName" component={Restricted(EditSystemContainer, store)} />
|
||||
<Route path="/create-system" component={Restricted(CreateSystemContainer, store)} />
|
||||
<Route path="/list-systems" component={Restricted(ListSystemsContainer, store)} />
|
||||
|
||||
<Route path="/system/:shortName" component={Restricted(SystemContainer, store)} />
|
||||
<Route path="/update-system/:shortName" component={EditSystemContainer} />
|
||||
<Route path="/create-system" component={CreateSystemContainer} />
|
||||
<Route path="/list-systems" component={ListSystemsContainer} />
|
||||
*/
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<div>
|
||||
<App>
|
||||
<Switch>
|
||||
|
||||
<Route exact path="/" component={Restricted(ProfileContainer, store)} />
|
||||
<Route path="/login" component={LoginContainer} />
|
||||
<Route path="/register" component={RegisterContainer} />
|
||||
<Route path="/wait-confirm-email" component={WaitConfirmEmailContainer} />
|
||||
<Route path="/sent-confirm-email" component={SentConfirmEmailContainer} />
|
||||
<Route path="/send-reset-password" component={SendResetPasswordContainer} />
|
||||
<Route path="/reset-password/:userId/:token" component={ResetPasswordContainer} />
|
||||
<Route path="/confirm-email/:userId/:token" component={ConfirmEmailContainer} />
|
||||
<Route path="/terms" component={Restricted(TermsContainer, store)} />
|
||||
|
||||
<Route path="/billing" component={Restricted(BillingContainer, store)} />
|
||||
<Route path="/profile" component={Restricted(ProfileContainer, store)} />
|
||||
<Route path="/update-plans" component={Restricted(UpdatePlans, store)} />
|
||||
</Switch>
|
||||
</App>
|
||||
</div>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
, document.getElementById("app"))
|
||||
@@ -1,31 +0,0 @@
|
||||
import React, { Component } from "react"
|
||||
|
||||
class Default extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div>
|
||||
<h2>React-Passport-Redux-Example</h2>
|
||||
<p>Demonstration of PassportJS authentication in React w/ Redux. Use the links above to register a new user and login/logout. You will only be able to view the MyProfile page when you are logged in. Logging in will redirect you to MyProfile if you had clicked that link beforehand.</p>
|
||||
<p>I'll be doing my best to document code with useful comments so look for more detail there. Note: you should be able to implement the authentication functionality using less packages. I was working on another project before deciding to make this example repo so some bloat may have travelled over from that.</p>
|
||||
<p>In full, this repo demonstrates:</p>
|
||||
<ul>
|
||||
<li><strong>React</strong> for the view layer </li>
|
||||
<li><strong>PassportJS</strong> for authentication (using a Local Strategy)</li>
|
||||
<li><strong>Redux</strong> to handle our state</li>
|
||||
<li><strong>React Router</strong> for routing client-side</li>
|
||||
<li><strong>Express</strong> for handling server requests (REST and page requests)</li>
|
||||
<li><strong>MongoDB</strong> for our database, with <strong>Mongoose</strong> handling our schema</li>
|
||||
<li><strong>Webpack</strong> to bundle our client-side code</li>
|
||||
<li>Hot reloading using <strong>webpack-dev-middleware</strong> and <strong>webpack-hot-middleware</strong></li>
|
||||
</ul>
|
||||
<p>Shoutout to GitHub member <b>choonkending</b> whose <a href="https://github.com/choonkending/react-webpack-node" target="_blank">repo</a> was a big help. Pull requests and comments / issue reports are most welcome!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Default
|
||||
@@ -1,5 +0,0 @@
|
||||
.field {
|
||||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { combineReducers } from "redux"
|
||||
import { connectRouter } from 'connected-react-router'
|
||||
import user from "../User/user-reducer"
|
||||
import system from "../System/system-reducer"
|
||||
import billing from "../Billing/billing-reducer"
|
||||
|
||||
export default (history) => combineReducers({
|
||||
router: connectRouter(history),
|
||||
user,
|
||||
system,
|
||||
billing
|
||||
})
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { Button } from '@storybook/react/demo';
|
||||
import InvoiceList from '../Billing/InvoiceList';
|
||||
import 'semantic-ui-css/semantic.min.css'
|
||||
|
||||
storiesOf('Button', module)
|
||||
.add('with text', () => (
|
||||
<Button>Hello Button</Button>
|
||||
))
|
||||
.add('with some emoji', () => (
|
||||
<Button><span role="img" aria-label="so cool">😀 😎 👍 💯</span></Button>
|
||||
));
|
||||
|
||||
|
||||
export const invoices = [{"amount_due":1500,"amount_paid":1500,"date":1548819212,"invoice_pdf":"https://pay.stripe.com/invoice/invst_BYlZ3MlhYMHEStbvdVJ6vddxgp/pdf","paid":true,"status":"paid","description":null},{"amount_due":1500,"amount_paid":1500,"date":1548819083,"invoice_pdf":"https://pay.stripe.com/invoice/invst_ts0VnXKuxHWHce02yjhu0Cz455/pdf","paid":true,"status":"paid","description":null},{"amount_due":1500,"amount_paid":1500,"date":1548818978,"invoice_pdf":"https://pay.stripe.com/invoice/invst_HSjke6BJAWGlZL8ggn3Jgb8pnT/pdf","paid":true,"status":"paid","description":null},{"amount_due":6500,"amount_paid":6500,"date":1548816924,"invoice_pdf":"https://pay.stripe.com/invoice/invst_joLNjLFGU3PIondKAAop6kJaSZ/pdf","paid":true,"status":"paid","description":null},{"amount_due":1500,"amount_paid":1500,"date":1548772157,"invoice_pdf":"https://pay.stripe.com/invoice/invst_cSWcbq3UKUHxPQChSjkxRKx6gO/pdf","paid":true,"status":"paid","description":null},{"amount_due":1500,"amount_paid":1500,"date":1548772077,"invoice_pdf":"https://pay.stripe.com/invoice/invst_1aMYlwnPOdXKbebGuCjQfUrsg2/pdf","paid":true,"status":"paid","description":null}];
|
||||
|
||||
storiesOf('InvoiceList', module).add("standard", () => {
|
||||
return (<InvoiceList invoices={invoices}/>)
|
||||
|
||||
});
|
||||
48
account/src/index.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux'
|
||||
import { Route, Switch } from 'react-router' // react-router v4/v5
|
||||
import { ConnectedRouter } from 'connected-react-router'
|
||||
import configureStore, { history } from './redux-router/configureStore'
|
||||
import 'semantic-ui-css/semantic.min.css'
|
||||
import App from "./App/App"
|
||||
import Restricted from "./App/Restricted"
|
||||
|
||||
import LoginContainer from "./User/LoginContainer"
|
||||
import RegisterContainer from "./User/RegisterContainer"
|
||||
import ProfileContainer from "./User/ProfileContainer"
|
||||
import ConfirmEmailContainer from "./User/ConfirmEmailContainer"
|
||||
import WaitConfirmEmailContainer from "./User/WaitConfirmEmailContainer"
|
||||
import SentConfirmEmailContainer from "./User/SentConfirmEmailContainer"
|
||||
import SendResetPasswordContainer from "./User/SendResetPasswordContainer"
|
||||
import ResetPasswordContainer from "./User/ResetPasswordContainer"
|
||||
import TermsContainer from "./User/TermsContainer"
|
||||
|
||||
const store = configureStore(/* provide initial state if any */)
|
||||
|
||||
console.log(process.env);
|
||||
//This thssi
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ }
|
||||
<>
|
||||
<App>
|
||||
<Switch>
|
||||
<Route exact path="/" component={Restricted(ProfileContainer, store)} />
|
||||
<Route path="/login" component={LoginContainer} />
|
||||
<Route path="/register" component={RegisterContainer} />
|
||||
<Route path="/wait-confirm-email" component={WaitConfirmEmailContainer} />
|
||||
<Route path="/sent-confirm-email" component={SentConfirmEmailContainer} />
|
||||
<Route path="/send-reset-password" component={SendResetPasswordContainer} />
|
||||
<Route path="/reset-password/:userId/:token" component={ResetPasswordContainer} />
|
||||
<Route path="/confirm-email/:userId/:token" component={ConfirmEmailContainer} />
|
||||
<Route path="/terms" component={Restricted(TermsContainer, store)} />
|
||||
<Route path="/profile" component={Restricted(ProfileContainer, store)} />
|
||||
|
||||
</Switch>
|
||||
</App>
|
||||
</>
|
||||
</ConnectedRouter>
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
26
account/src/redux-router/configureStore.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// https://github.com/supasate/connected-react-router
|
||||
// configureStore.js
|
||||
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { applyMiddleware, compose, createStore } from 'redux'
|
||||
import { routerMiddleware } from 'connected-react-router'
|
||||
import createRootReducer from './reducers'
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
export const history = createBrowserHistory()
|
||||
|
||||
export default function configureStore(preloadedState) {
|
||||
const store = createStore(
|
||||
createRootReducer(history), // root reducer with router state
|
||||
preloadedState,
|
||||
compose(
|
||||
applyMiddleware(
|
||||
routerMiddleware(history), // for dispatching history actions
|
||||
thunk,
|
||||
// ... other middlewares ...
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return store
|
||||
}
|
||||
13
account/src/redux-router/reducers.js
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
// from https://github.com/supasate/connected-react-router
|
||||
import { combineReducers } from 'redux'
|
||||
import { connectRouter } from 'connected-react-router'
|
||||
import user from "../User/user-reducer"
|
||||
|
||||
const createRootReducer = (history) => combineReducers({
|
||||
router: connectRouter(history),
|
||||
user,
|
||||
})
|
||||
export default createRootReducer
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,60 +0,0 @@
|
||||
import path from "path"
|
||||
import express from "express"
|
||||
import session from "express-session"
|
||||
import bodyParser from "body-parser"
|
||||
import connectMongo from "connect-mongo"
|
||||
import secrets from "./secrets"
|
||||
|
||||
const MongoStore = connectMongo(session)
|
||||
var cookie_domain = process.env['COOKIE_DOMAIN'] != null ? process.env['COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
|
||||
|
||||
export default function(app, passport) {
|
||||
app.set("port", 3009)
|
||||
|
||||
// X-Powered-By header has no functional value.
|
||||
// Keeping it makes it easier for an attacker to build the site's profile
|
||||
// It can be removed safely
|
||||
app.disable("x-powered-by")
|
||||
app.enable('trust proxy')
|
||||
app.use(bodyParser.json())
|
||||
app.use(bodyParser.urlencoded({ extended: true }))
|
||||
app.use(express.static(path.join(process.cwd(), 'public')));
|
||||
|
||||
const sess = {
|
||||
secret: 'secret',
|
||||
/*saveUninitialized: false,
|
||||
resave: false,*/
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
secret: secrets.sessionSecret,
|
||||
proxy: true,
|
||||
name: "sessionId",
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
domain: cookie_domain
|
||||
},
|
||||
store: new MongoStore({
|
||||
url: secrets.db,
|
||||
autoReconnect: true
|
||||
})
|
||||
}
|
||||
|
||||
var node_env = process.env.NODE_ENV;
|
||||
console.log('--------------------------');
|
||||
console.log('===> 😊 Starting Server . . .');
|
||||
console.log('===> Environment: ' + node_env);
|
||||
/*if(node_env === 'production') {
|
||||
console.log('===> 🚦 Note: In order for authentication to work in production');
|
||||
console.log('===> you will need a secure HTTPS connection');
|
||||
sess.cookie.secure = true; // Serve secure cookies
|
||||
sess.cookie.httpOnly = false;
|
||||
}*/
|
||||
|
||||
app.use(session(sess))
|
||||
|
||||
app.use(passport.initialize())
|
||||
app.use(passport.session())
|
||||
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
import Plan from "../models/plan";
|
||||
import System from "../models/system";
|
||||
|
||||
const stripeKeyPublishable = process.env.STRIPE_PUBLISHABLE_KEY;
|
||||
const stripeKeySecret = process.env.STRIPE_SECRET_KEY;
|
||||
|
||||
const stripe = require("stripe")(stripeKeySecret);
|
||||
|
||||
var admin_email = process.env['ADMIN_EMAIL'] != null ? process.env['ADMIN_EMAIL'] : "luke@openmhz.com";
|
||||
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
|
||||
var account_server = process.env['ACCOUNT_SERVER'] != null ? process.env['ACCOUNT_SERVER'] : "https://account.openmhz.com";
|
||||
var cookie_domain = process.env['COOKIE_DOMAIN'] != null ? process.env['COOKIE_DOMAIN'] : '.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
|
||||
exports.isLoggedIn = function (req, res, next) {
|
||||
if (req.isAuthenticated()) return next();
|
||||
res.redirect("/login");
|
||||
};
|
||||
|
||||
|
||||
function makeSystemsResponse(systems) {
|
||||
var returnSys = systems.map(obj => {
|
||||
var rObj = (({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
userId,
|
||||
key,
|
||||
planType,
|
||||
showScreenName
|
||||
}) => ({
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
systemType,
|
||||
city,
|
||||
state,
|
||||
county,
|
||||
country,
|
||||
userId,
|
||||
key,
|
||||
planType,
|
||||
showScreenName
|
||||
}))(obj);
|
||||
rObj.id = obj._id;
|
||||
return rObj;
|
||||
});
|
||||
return returnSys;
|
||||
}
|
||||
|
||||
function findSubscriptionItem(subscription, planId) {
|
||||
var items = subscription.items.data;
|
||||
|
||||
var subscriptionItem = items.find(function (element) {
|
||||
return element.plan.id == planId;
|
||||
});
|
||||
return subscriptionItem;
|
||||
};
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
// makes sure the plans a user is trying to update actually belong to them.
|
||||
function validatePlans(systems, updatedPlans) {
|
||||
var validatedPlans = [];
|
||||
systems.forEach(function (system) {
|
||||
|
||||
// Find the updated plan for that system.
|
||||
const updatedPlan = updatedPlans.find(function (element) {
|
||||
return element.shortName == system.shortName;
|
||||
})
|
||||
if (!updatedPlan) {
|
||||
console.log("ERROR: updated plan not found for ShortName");
|
||||
}
|
||||
|
||||
// Update that systems plan type
|
||||
// TODO: add validation of plantype
|
||||
system.planType = updatedPlan.planType;
|
||||
validatedPlans.push({ shortName: system.shortName, planType: updatedPlan.planType });
|
||||
});
|
||||
return validatedPlans;
|
||||
}
|
||||
exports.updatePlans = async function (req, res, next) {
|
||||
const userId = mongoose.Types.ObjectId(req.user._id);
|
||||
|
||||
if (!req.user.customerId) {
|
||||
console.log("ERROR: user does not have billing setup");
|
||||
res.json({
|
||||
success: false,
|
||||
message: "ERROR: user does not have billing setup"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedPlans = req.body.updatePlans;
|
||||
console.log(updatedPlans);
|
||||
|
||||
try {
|
||||
var plan = await Plan.findOne({ userId: userId });
|
||||
|
||||
// looks like the User does not have a plan yet
|
||||
|
||||
if (!plan) {
|
||||
plan = new Plan({ userId: userId });
|
||||
}
|
||||
|
||||
|
||||
// Find all of the Users Systems
|
||||
var systems = await System.find({ userId: mongoose.Types.ObjectId(req.user._id) })
|
||||
|
||||
|
||||
} catch (err) {
|
||||
|
||||
console.log("Error upate plans: " + err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// For each of the User's Systems
|
||||
var validatedPlans = validatePlans(systems, updatedPlans);
|
||||
|
||||
// Create the subscription object
|
||||
console.log("---------Validated Plans--------");
|
||||
console.log(validatedPlans);
|
||||
var proPlans = { plan: "plan_EQoJeqkAw6FCgy", quantity: 0 };
|
||||
var proPlanSystems = [];
|
||||
var proPlanQuantity = 0;
|
||||
validatedPlans.forEach(function (plan) {
|
||||
if (plan.planType == 10) {
|
||||
proPlanQuantity++;
|
||||
proPlans.quantity++;
|
||||
proPlanSystems.push(plan.shortName);
|
||||
}
|
||||
});
|
||||
|
||||
if (!plan.subscriptionId) {
|
||||
//proPlans.metadata = proPlanSystems.join();
|
||||
var items = [];
|
||||
items.push(proPlans);
|
||||
//Subscribe the User
|
||||
|
||||
console.log(subscription);
|
||||
try {
|
||||
var subscription = await stripe.subscriptions.create({
|
||||
customer: req.user.customerId,
|
||||
items: items
|
||||
});
|
||||
console.log(subscription);
|
||||
|
||||
const subscriptionItem = findSubscriptionItem(subscription, "plan_EQoJeqkAw6FCgy");
|
||||
if (subscriptionItem) {
|
||||
plan.proSystemSubscription = { subscriptionItemId: subscriptionItem.id, quantity: proPlanQuantity, systems: proPlanSystems };
|
||||
plan.subscriptionId = subscription.id;
|
||||
await plan.save();
|
||||
systems.forEach(async function (system) { await system.save(); });
|
||||
// Save the updated Systems
|
||||
const returnSystems = makeSystemsResponse(systems);
|
||||
res.json({
|
||||
success: true,
|
||||
systems: returnSystems
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Error upate plans: " + err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// we alreay have a Subscription for the User
|
||||
try {
|
||||
var subscriptionItem=null;
|
||||
// Check if the user has a Pro Plan subscription Item setup yet.
|
||||
if (plan.proSystemSubscription) {
|
||||
subscriptionItem = await stripe.subscriptionItems.update(plan.proSystemSubscription.subscriptionItemId,
|
||||
{
|
||||
quantity: proPlanQuantity,
|
||||
});
|
||||
} else {
|
||||
// Create one if they don't
|
||||
subscriptionItem = await stripe.subscriptionItems.create(
|
||||
{
|
||||
subscription: plan.subscriptionId,
|
||||
plan: "plan_EQoJeqkAw6FCgy",
|
||||
quantity: proPlanQuantity,
|
||||
});
|
||||
}
|
||||
|
||||
if (subscriptionItem) {
|
||||
plan.proSystemSubscription = { subscriptionItemId: subscriptionItem.id, quantity: proPlanQuantity, systems: proPlanSystems };
|
||||
await plan.save();
|
||||
systems.forEach(async function (system) { await system.save(); });
|
||||
// Save the updated Systems
|
||||
const returnSystems = makeSystemsResponse(systems);
|
||||
res.json({
|
||||
success: true,
|
||||
systems: returnSystems
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.log("Error upate plans: " + err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.getInvoices = async function (req, res, next) {
|
||||
if (!req.user.customerId) {
|
||||
console.log("ERROR: user does not have billing setup");
|
||||
res.json({
|
||||
success: false,
|
||||
message: "ERROR: user does not have billing setup"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Trying to fetch invoices for: " + req.user.customerId);
|
||||
var invoices = await stripe.invoices.list( { limit: 12, customer: req.user.customerId });
|
||||
console.log(invoices);
|
||||
var obj = invoices.data.map(obj => {
|
||||
var rObj = (({
|
||||
amount_due,
|
||||
amount_paid,
|
||||
date,
|
||||
invoice_pdf,
|
||||
paid,
|
||||
status,
|
||||
description
|
||||
}) => ({
|
||||
amount_due,
|
||||
amount_paid,
|
||||
date,
|
||||
invoice_pdf,
|
||||
paid,
|
||||
status,
|
||||
description
|
||||
}))(obj);
|
||||
rObj.id = obj._id;
|
||||
return rObj;
|
||||
});
|
||||
return res.json({
|
||||
success: true,
|
||||
invoices: obj
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("Error retrieving licesnes: " + err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
import fs from "fs"
|
||||
import express from "express"
|
||||
import mongoose from "mongoose"
|
||||
import passport from "passport"
|
||||
import webpack from "webpack"
|
||||
import path from "path"
|
||||
import config from "../../webpack/webpack.config.dev.js"
|
||||
import secrets from "./config/secrets"
|
||||
import configurePassport from "./config/passport"
|
||||
import configureExpress from "./config/express"
|
||||
import users from "./controllers/users"
|
||||
import plans from "./controllers/plans"
|
||||
const stripeKeyPublishable = process.env.STRIPE_PUBLISHABLE_KEY;
|
||||
const stripeKeySecret = process.env.STRIPE_SECRET_KEY;
|
||||
|
||||
const stripe = require("stripe")(stripeKeySecret);
|
||||
import "./models/user"
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
const app = express()
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
const connect = () => {
|
||||
mongoose.connect(secrets.db, (err, res) => {
|
||||
if (err) {
|
||||
console.log(`Error connecting to ${secrets.db}. ${err}`)
|
||||
} else {
|
||||
console.log(`Successfully connected to ${secrets.db}.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
connect()
|
||||
|
||||
mongoose.connection.on("error", console.error)
|
||||
mongoose.connection.on("disconnected", connect)
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development"
|
||||
|
||||
// if in development mode set up the middleware required for hot reloading and rebundling
|
||||
if(isDev) {
|
||||
|
||||
const compiler = webpack(config)
|
||||
|
||||
app.use(require("webpack-dev-middleware")(compiler, {
|
||||
noInfo: true,
|
||||
publicPath: config.output.publicPath
|
||||
}))
|
||||
|
||||
app.use(require("webpack-hot-middleware")(compiler))
|
||||
}
|
||||
|
||||
|
||||
var backend_server = process.env['BACKEND_SERVER'] != null ? process.env['BACKEND_SERVER'] : 'https://api.openmhz.com';
|
||||
var frontend_server = process.env['FRONTEND_SERVER'] != null ? process.env['FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||
var media_server = process.env['MEDIA_SERVER'] != null ? process.env['MEDIA_SERVER'] : 'https://media.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var socket_server = process.env['SOCKET_SERVER'] != null ? process.env['SOCKET_SERVER'] : 'wss://api.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var admin_server = process.env['ADMIN_SERVER'] != null ? process.env['ADMIN_SERVER'] : 'https://admin.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var account_server = process.env['ACCOUNT_SERVER'] != null ? process.env['ACCOUNT_SERVER'] : 'https://account.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
|
||||
const proPlanValue = process.env['PRO_PLAN'] != null ? process.env['PRO_PLAN'] : 10;
|
||||
const freePlanValue = process.env['FREE_PLAN'] != null ? process.env['FREE_PLAN'] : 0;
|
||||
const proPlanPrice = process.env['PRO_PLAN_PRICE'] != null ? process.env['PRO_PLAN_PRICE'] : 15;
|
||||
const freePlanPrice = process.env['FREE_PLAN_PRICE'] != null ? process.env['FREE_PLAN_PRICE'] : 0;
|
||||
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
configurePassport(app, passport)
|
||||
configureExpress(app, passport)
|
||||
|
||||
|
||||
app.all('*', function(req, res, next) {
|
||||
var allowedOrigins = [account_server, admin_server, frontend_server];
|
||||
var origin = req.headers.origin;
|
||||
if(allowedOrigins.indexOf(origin) > -1){
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
res.header("Access-Control-Allow-Credentials", "true");
|
||||
res.header("Access-Control-Allow-Headers", "X-Requested-With");
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type');
|
||||
next();
|
||||
});
|
||||
|
||||
// -------------------------------------------
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
app.post("/login", users.login)
|
||||
app.get("/logout", users.logout)
|
||||
app.get("/authenticated", users.authenticated)
|
||||
app.post("/register", users.validateProfile, users.register)
|
||||
app.post("/users/:userId/reset-password/:token", users.resetPassword)
|
||||
app.post("/api/send-reset-password", users.sendResetPassword)
|
||||
app.post("/users/:userId", users.isLoggedIn, users.validateProfile, users.updateProfile)
|
||||
app.post("/users/:userId/terms", users.isLoggedIn, users.terms)
|
||||
app.post("/users/:userId/send-confirm", users.sendConfirmEmail)
|
||||
app.post("/users/:userId/confirm/:token", users.confirmEmail)
|
||||
/*app.get("/api/invoices", users.isLoggedIn, plans.getInvoices)
|
||||
app.post("/api/plans", users.isLoggedIn, plans.updatePlans)
|
||||
app.get("/api/billing", users.isLoggedIn, (req,res) => {
|
||||
if (req.user.customerId) {
|
||||
stripe.customers.retrieve(
|
||||
req.user.customerId,
|
||||
function(err, customer) {
|
||||
if (err) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
}
|
||||
if (customer) {
|
||||
if (customer.sources.total_count < 1) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "Payment sources not defined"
|
||||
});
|
||||
}
|
||||
|
||||
for (var i=0; i < customer.sources.data.length; i++) {
|
||||
|
||||
|
||||
var source = customer.sources.data[i];
|
||||
if (source.id == customer.default_source) {
|
||||
console.log("match on: " + i);
|
||||
var obj = {
|
||||
name: source.owner.name,
|
||||
brand: source.card.brand,
|
||||
country: source.card.country,
|
||||
exp_month: source.card.exp_month,
|
||||
exp_year: source.card.exp_year,
|
||||
last4: source.card.last4
|
||||
}
|
||||
return res.json({
|
||||
success: true,
|
||||
source: obj
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "Default payment source not found"
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "User does not have a Customer ID"
|
||||
});
|
||||
}
|
||||
});*/
|
||||
|
||||
function updateCustomerId(req, res, err, customer) {
|
||||
if (err){
|
||||
return res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
}
|
||||
req.user.customerId = customer.id;
|
||||
req.user.save(err => {
|
||||
if (err) {
|
||||
console.error("Error saving customer ID");
|
||||
console.error(err);
|
||||
res.json({
|
||||
success: false,
|
||||
message: err
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
console.log(customer);
|
||||
if (customer.sources.total_count < 1) {
|
||||
return res.json({
|
||||
success: false,
|
||||
message: "Payment sources not defined"
|
||||
});
|
||||
}
|
||||
|
||||
for (var i=0; i < customer.sources.data.length; i++) {
|
||||
console.log("--------Customer-----------");
|
||||
console.log(customer);
|
||||
|
||||
var source = customer.sources.data[i];
|
||||
if (source.id == customer.default_source) {
|
||||
console.log("--------Source-----------");
|
||||
console.log(source);
|
||||
var obj = {
|
||||
name: source.owner.name,
|
||||
brand: source.card.brand,
|
||||
country: source.card.country,
|
||||
exp_month: source.card.exp_month,
|
||||
exp_year: source.card.exp_year,
|
||||
last4: source.card.last4
|
||||
}
|
||||
return res.json({
|
||||
success: true,
|
||||
source: obj
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
app.post("/api/customer", users.isLoggedIn, (req, res) => {
|
||||
|
||||
//
|
||||
if (req.user.customerId) {
|
||||
stripe.customers.update(req.user.customerId, {
|
||||
source: req.body.sourceId
|
||||
}, function(err, customer) {
|
||||
return updateCustomerId(req, res, err, customer);
|
||||
});
|
||||
} else {
|
||||
stripe.customers.create({
|
||||
email: req.user.email,
|
||||
source: req.body.sourceId
|
||||
}, function(err, customer) {
|
||||
return updateCustomerId(req, res, err, customer);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get("*", (req, res, next) => {
|
||||
|
||||
// if we are in production mode then an extension will be provided, usually ".min"
|
||||
const minified = process.env.MIN_EXT || ""
|
||||
|
||||
// this is the HTML we will send to the client when they request any page. React and React Router
|
||||
// will take over once the scripts are loaded client-side
|
||||
const appHTML =
|
||||
`<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
<title>OpenMHz - Account</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/style/style.css"></link>
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.1/dist/semantic.min.css" />
|
||||
<script>
|
||||
const frontend_server = "${frontend_server}";
|
||||
const admin_server = "${admin_server}";
|
||||
const backend_server = "${backend_server}";
|
||||
const media_server = "${media_server}";
|
||||
const socket_server = "${socket_server}";
|
||||
const account_server = "${account_server}";
|
||||
const site_name = "${site_name}";
|
||||
const proPlanValue = ${proPlanValue};
|
||||
const freePlanValue = ${freePlanValue};
|
||||
const proPlanPrice = ${proPlanPrice};
|
||||
const freePlanPrice = ${freePlanPrice};
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="/assets/app${minified}.js"></script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
res.status(200).end(appHTML)
|
||||
|
||||
})
|
||||
|
||||
// start listening to incoming requests
|
||||
app.listen(app.get("port"), app.get("host"), (err) => {
|
||||
if (err) {
|
||||
console.err(err.stack)
|
||||
} else {
|
||||
console.log(`App listening on port ${app.get("port")} [${process.env.NODE_ENV} mode]`)
|
||||
}
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
// Defining a User Model in mongoose
|
||||
// Code modified from https://github.com/sahat/hackathon-starter
|
||||
import bcrypt from "bcrypt-nodejs"
|
||||
import mongoose from "mongoose"
|
||||
|
||||
|
||||
var SubscriptionSchema = mongoose.Schema({ subscriptionItemId: String, quantity: {type: Number, default: 0}, systems: [String] });
|
||||
const PlanSchema = new mongoose.Schema({
|
||||
|
||||
userId: mongoose.Schema.Types.ObjectId,
|
||||
userPlan: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
subscriptionId: String,
|
||||
proSystemSubscription: SubscriptionSchema
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export default mongoose.model("Plan", PlanSchema)
|
||||
@@ -1,18 +0,0 @@
|
||||
db = db.getMongo().getDB( "scanner" );
|
||||
|
||||
|
||||
|
||||
db.users.find().forEach(
|
||||
function(user){
|
||||
|
||||
db.systems.updateMany(
|
||||
{ "user": { $eq: user.email } },
|
||||
{
|
||||
$set: { "userId": user._id, "showScreenName": false },
|
||||
}
|
||||
);
|
||||
|
||||
print("Upgrading: " + user.email);
|
||||
|
||||
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
var path = require("path")
|
||||
var webpack = require("webpack")
|
||||
var assetsPath = path.join(__dirname, "..", "public", "assets")
|
||||
var publicPath = "/assets/"
|
||||
|
||||
module.exports = {
|
||||
name: "browser",
|
||||
mode: 'development',
|
||||
devtool: "eval-source-map",
|
||||
|
||||
// The base directory (absolute path!) for resolving the entry option.
|
||||
context: path.join(__dirname, "..", "src", "client"),
|
||||
|
||||
entry: {
|
||||
app: [
|
||||
"./client",
|
||||
"webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true"
|
||||
]
|
||||
},
|
||||
|
||||
optimization: {
|
||||
minimize: false
|
||||
},
|
||||
|
||||
output: {
|
||||
path: assetsPath,
|
||||
filename: "[name].js",
|
||||
publicPath: publicPath
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
}],
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: [".js", ".jsx"],
|
||||
modules: [
|
||||
path.resolve('src'),
|
||||
'node_modules',
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
"process.env": {
|
||||
NODE_ENV: JSON.stringify("development")
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
var path = require("path")
|
||||
var webpack = require("webpack")
|
||||
var assetsPath = path.join(__dirname, "..", "public", "assets")
|
||||
var publicPath = "/assets/"
|
||||
|
||||
module.exports = {
|
||||
// The configuration for the client
|
||||
name: "browser",
|
||||
mode: 'production',
|
||||
devtool: "source-map",
|
||||
|
||||
// The base directory (absolute path!) for resolving the entry option.
|
||||
context: path.join(__dirname, "..", "src", "client"),
|
||||
|
||||
optimization: {
|
||||
minimize: true
|
||||
},
|
||||
entry: {
|
||||
app: "./client"
|
||||
},
|
||||
|
||||
output: {
|
||||
path: assetsPath,
|
||||
filename: "[name].min.js",
|
||||
publicPath: publicPath
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: [".js", ".jsx"],
|
||||
modules: [
|
||||
path.resolve('src'),
|
||||
'node_modules',
|
||||
],
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
}],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env": {
|
||||
NODE_ENV: JSON.stringify("production")
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
10354
account/yarn.lock
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets":
|
||||
["@babel/preset-env",
|
||||
"@babel/react"],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
["@babel/plugin-transform-runtime", {
|
||||
"regenerator": true
|
||||
}],
|
||||
["lodash", { "id": ["lodash", "semantic-ui-react"] }]
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
node_modules
|
||||
lib
|
||||
public
|
||||
/node_modules
|
||||
/build
|
||||
1
admin/.eslintcache
Normal file
59
admin/.gitignore
vendored
@@ -1,36 +1,23 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
lib
|
||||
uploads/
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
assets
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
# docker build -t trunk-server-frontend .
|
||||
# do a `git pull` in trunk-server-frontend to update
|
||||
|
||||
FROM node:10
|
||||
|
||||
|
||||
ENV HOME=/home/app
|
||||
|
||||
RUN mkdir -p /home/app
|
||||
COPY ./package.json /tmp
|
||||
RUN cd /tmp && npm install
|
||||
RUN cp -a /tmp/node_modules /home/app
|
||||
|
||||
# add `/usr/src/app/node_modules/.bin` to $PATH
|
||||
ENV PATH /usr/app/node_modules/.bin:$PATH
|
||||
|
||||
WORKDIR $HOME
|
||||
COPY . $HOME/
|
||||
# build environment
|
||||
FROM node:13.12.0-alpine as build
|
||||
WORKDIR /app
|
||||
ENV PATH /app/node_modules/.bin:$PATH
|
||||
ARG REACT_APP_BACKEND_SERVER
|
||||
ARG REACT_APP_ADMIN_SERVER
|
||||
ARG REACT_APP_ACCOUNT_SERVER
|
||||
ARG REACT_APP_FRONTEND_SERVER
|
||||
ARG REACT_APP_SITE_NAME
|
||||
ARG REACT_APP_ADMIN_EMAIL
|
||||
ARG NODE_ENV
|
||||
COPY package.json ./
|
||||
RUN npm install --also=dev
|
||||
COPY . ./
|
||||
RUN npm run build
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
# production environment
|
||||
FROM node:13.12.0-alpine
|
||||
RUN mkdir -p /app/public
|
||||
COPY ./package.json /tmp
|
||||
RUN cd /tmp && npm install
|
||||
RUN cp -a /tmp/node_modules /app
|
||||
WORKDIR /app
|
||||
COPY server /app
|
||||
COPY --from=build /app/build /app/public
|
||||
CMD ["node", "index.js"]
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
Called during development only. Allows us to use ES6 and JSX in our server code.
|
||||
*/
|
||||
|
||||
require('@babel/register')({
|
||||
presets: ["@babel/preset-env",
|
||||
"@babel/react"]
|
||||
});
|
||||
|
||||
require("./src/server/index")
|
||||
10
admin/notes.md
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
## Base
|
||||
started development using `npx create-react-app admin --template redux`
|
||||
|
||||
## Packages used:
|
||||
- [connected-react-router](https://github.com/supasate/connected-react-router): Synchronize router state with redux store through
|
||||
- [Redux Thunk](https://github.com/reduxjs/redux-thunk): lets you do AJAX calls
|
||||
- [axios](https://github.com/axios/axios) - library for doing HTTP requests
|
||||
- [react-router-dom](https://reactrouter.com/web/guides/quick-start) - Adds routes to React
|
||||
- [Semantic UI](https://react.semantic-ui.com/usage) - The UI frontend
|
||||