├── backend
├── database
│ └── .gitkeep
├── views
│ ├── index.jade
│ ├── error.jade
│ └── layout.jade
├── .env.example
├── public
│ └── stylesheets
│ │ └── style.css
├── routes
│ ├── getDatabase.js
│ ├── message.js
│ ├── isInDatabase.js
│ ├── unregisterUser.js
│ ├── registerUser.js
│ └── verifySignature.js
├── package.json
├── bin
│ └── www
├── README.md
├── app.js
└── package-lock.json
├── .gitignore
├── frontend
├── src
│ ├── index.css
│ ├── index.js
│ ├── App.test.js
│ ├── App.css
│ ├── registerServiceWorker.js
│ ├── hydroLogo.svg
│ └── App.js
├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
├── .gitignore
├── package.json
└── README.md
├── LICENSE
└── README.md
/backend/database/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | *.sqlite
3 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/backend/views/index.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= title
5 | p Welcome to #{title}
6 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hydrogen-dev/raindrop-ui-web/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/backend/.env.example:
--------------------------------------------------------------------------------
1 | clientId=yourId
2 | clientSecret=yourSecret
3 | applicationId=yourApplicationId
4 | hydroEnvironment=Sandbox
5 |
--------------------------------------------------------------------------------
/backend/views/error.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/backend/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= title
5 | link(rel='stylesheet', href='/stylesheets/style.css')
6 | body
7 | block content
8 |
--------------------------------------------------------------------------------
/backend/public/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4 | }
5 |
6 | a {
7 | color: #00B7FF;
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 | registerServiceWorker();
9 |
--------------------------------------------------------------------------------
/frontend/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Hydro Raindrop",
3 | "name": "Client-Side Raindrop",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/backend/routes/getDatabase.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | // returns the entire database...for demonstration purposes only
5 | router.get('/', function(req, res, next) {
6 | req.app.get('db').all("SELECT * FROM hydro2FA", [], (error, rows) => {
7 | if (error) {
8 | console.log(error)
9 | }
10 | res.json(rows ? rows : [{}]);
11 | });
12 | });
13 |
14 | module.exports = router;
15 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "PORT=3001 node ./bin/www"
7 | },
8 | "dependencies": {
9 | "@hydrogenplatform/raindrop": "^0.2.7",
10 | "cookie-parser": "~1.4.3",
11 | "debug": "~2.6.9",
12 | "dotenv": "^5.0.1",
13 | "express": "~4.16.0",
14 | "express-session": "^1.15.6",
15 | "http-errors": "~1.6.2",
16 | "jade": "~1.11.0",
17 | "memorystore": "^1.6.0",
18 | "morgan": "~1.9.0",
19 | "sqlite3": "^4.0.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.4.1",
7 | "react-dom": "^16.4.1",
8 | "react-qr-svg": "^2.1.0",
9 | "react-scripts": "1.1.4",
10 | "react-toggle": "^4.0.2",
11 | "ts-react-json-table": "^0.1.1"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "test": "react-scripts test --env=jsdom",
17 | "eject": "react-scripts eject"
18 | },
19 | "proxy": "http://localhost:3001"
20 | }
21 |
--------------------------------------------------------------------------------
/backend/routes/message.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 | var raindrop = require('@hydrogenplatform/raindrop');
4 |
5 | // gets a randomly generated message for the user to sign
6 | router.get('/', function(req, res, next) {
7 | let message = raindrop.client.generateMessage();
8 | req.session.message = message; // save the message in the session
9 | req.session.save(function(error) {
10 | if (error) {
11 | console.log(error)
12 | res.send(400)
13 | return
14 | }
15 | res.json({message: message})
16 | })
17 | });
18 |
19 | module.exports = router;
20 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Frontend
2 |
3 | ## Getting started
4 | The frontend was generated with [create-react-app](https://github.com/facebook/create-react-app). It's a dynamic webpage that integrates with the backend through the backend's public-facing API. It also receives data from the Hydro API via the backend (the frontend cannot communicate directly with the Hydro API because it cannot store credentials without exposing them to the internet). All logic is contained in [src/App.js](./src/App.js).
5 |
6 | ## Setup
7 | - `npm install`
8 | - `npm start`
9 |
10 | The frontend should now be live on [port 3000]((http://localhost:3000/)). If something goes wrong, please open a Github issue or submit a PR.
11 |
12 | ## Code
13 | The logic is contained in [src/App.js](./src/App.js).
14 |
15 | ## Copyright & License
16 | Copyright 2018 The Hydrogen Technology Corporation under the MIT License.
17 |
--------------------------------------------------------------------------------
/backend/routes/isInDatabase.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | // returns the hydro username that is linked to the internal user, according to the database
5 | router.post('/', async function(req, res, next) {
6 | // WARNING: THE FOLLOWING LINE IS NOT PRODUCTION-SAFE.
7 | // Backend logic should not trust data passed in from a front-end. Rely on server-side sessions instead.
8 | let internalUsername = req.body.internalUsername;
9 |
10 | // get the user's information from the hydro2FA database
11 | var userInformation;
12 | req.app.get('db').get(
13 | "SELECT * FROM hydro2FA WHERE internalUsername = ?", internalUsername, (error, userInformation) =>
14 | {
15 | if (error) {
16 | console.log(error)
17 | }
18 |
19 | if (userInformation === undefined) {
20 | res.json({exists: false})
21 | } else {
22 | res.json({exists: true, hydroID: userInformation.hydroID, confirmed: userInformation.confirmed})
23 | }
24 | });
25 | });
26 |
27 | module.exports = router;
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Hydrogen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 100px;
7 | }
8 |
9 | .App-header {
10 | background-color: white;
11 | height: 150px;
12 | padding: 20px;
13 | color: black;
14 | }
15 |
16 | .App-title {
17 | font-size: 1.5em;
18 | }
19 |
20 | .App-intro {
21 | font-size: large;
22 | }
23 |
24 | .result-box {
25 | border: 1px solid black;
26 | border-radius: 10px;
27 | width: 30%;
28 | margin: auto;
29 | height: 1.5em;
30 | line-height: 1.5em;
31 | }
32 |
33 | .hidden {
34 | visibility: hidden;
35 | }
36 |
37 | /* override toggle styles */
38 | .react-toggle {
39 | vertical-align: bottom;
40 | }
41 | .react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
42 | background-color: #3072ED;
43 | }
44 |
45 | .react-toggle--checked .react-toggle-track {
46 | background-color: #4F8FF3;
47 | }
48 |
49 | .react-toggle-thumb {
50 | border: 1px solid #4F8FF3;
51 | }
52 |
53 | .react-toggle--checked .react-toggle-thumb {
54 | left: 27px;
55 | border-color: #4F8FF3;
56 | }
57 |
58 | table {
59 | margin: auto;
60 | }
61 |
62 | hr {
63 | width: 50%;
64 | }
65 |
66 | form {
67 | padding-right: 10px;
68 | }
69 |
70 | div {
71 | padding-top: 5px;
72 | padding-right: 5px;
73 | padding-bottom: 5px;
74 | padding-left: 5px;
75 | }
76 |
77 | .text {
78 | width: 75%;
79 | display: table;
80 | margin: auto;
81 | }
82 |
--------------------------------------------------------------------------------
/backend/routes/unregisterUser.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | // unregisters an internal user and record it in the database
5 | router.post('/', async function(req, res, next) {
6 | // WARNING: THE FOLLOWING LINE IS NOT PRODUCTION-SAFE.
7 | // Backend logic should not trust data passed in from a front-end. Rely on server-side sessions instead.
8 | let internalUsername = req.body.internalUsername;
9 |
10 | // get the internal user's hydro username from the database
11 | let hydroID = await new Promise((resolve, reject) => {
12 | req.app.get('db').get(
13 | "SELECT * FROM hydro2FA WHERE internalUsername = ?", [ internalUsername ], (error, userInformation) =>
14 | {
15 | if (error) {
16 | console.log(error)
17 | resolve()
18 | } else {
19 | resolve(userInformation.hydroID)
20 | }
21 | })
22 | });
23 |
24 | if (!hydroID) {
25 | res.sendStatus(404)
26 | return
27 | }
28 |
29 | // call the Hydro API with the internal user's linked HydroID
30 | req.app.get('ClientRaindropPartner').unregisterUser(hydroID)
31 | // if the API call to unregister was successful, delete it in the database
32 | .then(result => {
33 | req.app.get('db').run("DELETE FROM hydro2FA WHERE hydroID = ?", [ hydroID ], (error) => {
34 | if (error) {
35 | console.log("Manual deletion from the database may be required.", error)
36 | res.sendStatus(404)
37 | } else {
38 | res.json({unregistered: true})
39 | }
40 | });
41 | })
42 | .catch(error => {
43 | console.log(error)
44 | res.sendStatus(404)
45 | });
46 | });
47 |
48 | module.exports = router;
49 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Raindrop Demo
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/backend/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('backend:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/backend/routes/registerUser.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | // registers an internal user if the Hydro API call with their claimed username succeeds, records it in the database
5 | router.post('/', async function(req, res, next) {
6 | let hydroID = req.body.hydroID;
7 | // WARNING: THE FOLLOWING LINE IS NOT PRODUCTION-SAFE.
8 | // Backend logic should not trust data passed in from a front-end. Rely on server-side sessions instead.
9 | let internalUsername = req.body.internalUsername;
10 |
11 | // fail if the internal user already has a linked hydro username
12 | let canRegister = await new Promise((resolve, reject) => {
13 | req.app.get('db').get(
14 | "SELECT * FROM hydro2FA WHERE internalUsername = ?", [ internalUsername ], (error, userInformation) =>
15 | {
16 | if (userInformation !== undefined) {
17 | resolve(false)
18 | } else {
19 | resolve(true)
20 | }
21 | })
22 | });
23 | if (!canRegister) {
24 | console.log("the internal username is already linked to a hydroID in the database")
25 | res.sendStatus(404)
26 | return
27 | }
28 |
29 | // call the Hydro API with the internal user's claimed HydroID
30 | req.app.get('ClientRaindropPartner').registerUser(hydroID)
31 | // if the API call to register the user was successful, save it in the database
32 | .then(result => {
33 | req.app.get('db').run(
34 | "INSERT INTO hydro2FA (internalUsername, hydroID, confirmed) VALUES (?, ?, ?)",
35 | [internalUsername, hydroID, false], async error =>
36 | {
37 | if (error) {
38 | console.log(error)
39 | await req.app.get('ClientRaindropPartner').unregisterUser(hydroID) // unregister if there was a DB error
40 | res.sendStatus(404)
41 | } else {
42 | res.json({registered: true})
43 | }
44 | })
45 | })
46 | .catch(error => {
47 | console.log(error)
48 | res.sendStatus(404)
49 | })
50 | });
51 |
52 | module.exports = router;
53 |
--------------------------------------------------------------------------------
/backend/routes/verifySignature.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | // verifies signatures from internal users that have a registered hydro username
5 | router.post('/', async function(req, res, next) {
6 | let message = req.session.message; // get the message from the session
7 | console.log(`Verifying the following code: '${message}'`)
8 | // WARNING: THE FOLLOWING LINE IS NOT PRODUCTION-SAFE.
9 | // Backend logic should not trust data passed in from a front-end. Rely on server-side sessions instead.
10 | let internalUsername = req.body.internalUsername;
11 |
12 | // get the user's information from the hydro2FA database
13 | let userInformation = await new Promise((resolve,reject) => {
14 | req.app.get('db').get("SELECT * FROM hydro2FA WHERE internalUsername = ?", [internalUsername], (error, result) => {
15 | if (error) {
16 | console.log(error)
17 | resolve()
18 | } else {
19 | resolve(result)
20 | }
21 | })
22 | })
23 |
24 | // return false if the database doesn't contain a mapping of internal username to hydro username
25 | if (!userInformation) {
26 | console.log("User does not have a Hydro username associated with their account.");
27 | res.sendStatus(404)
28 | return
29 | }
30 |
31 | // if it does, call the Hydro API with the message and the hydroID
32 | req.app.get('ClientRaindropPartner').verifySignature(userInformation.hydroID, message)
33 | .then(async result => {
34 | if (!result.verified) {
35 | console.log("User did not sign the correct message.");
36 | res.json({verified: false})
37 | return
38 | }
39 |
40 | // if this was the first time the user verified a message, record it in the database
41 | if (userInformation.confirmed == 0) {
42 | let saved = await new Promise((resolve, reject) => {
43 | req.app.get('db').run(
44 | "UPDATE hydro2FA SET confirmed = 1 WHERE internalUsername = ?", [internalUsername], (error) => {
45 | if (error) {
46 | console.log(error)
47 | resolve(false)
48 | } else {
49 | resolve(true)
50 | }
51 | })
52 | })
53 | if (!saved) {
54 | console.log("User was authenticated with the Hydro API, but they could not be saved in the database")
55 | res.json({verified: false})
56 | return
57 | }
58 | }
59 |
60 | res.json({verified: true})
61 | })
62 | .catch((error) => {
63 | console.log(error.message)
64 | res.json({verified: false})
65 | })
66 | });
67 |
68 | module.exports = router;
69 |
--------------------------------------------------------------------------------
/backend/README.md:
--------------------------------------------------------------------------------
1 | # Backend
2 |
3 | ## Getting started
4 | This example backend runs on [express-generator](https://expressjs.com/en/starter/generator.html). It's a simple API that receives/serves data from/to the frontend. Initialization logic is found in `app.js`, and internal endpoints are defined in `routes/`. This backend is privileged, meaning that some of its public endpoints trigger calls to the Hydro API which are authenticated with secret credentials. These secrets, which **must not be exposed in the frontend**, are stored in a `.env` file (see [Setup](#setup) for details).
5 |
6 | The backend also integrates with a basic sqlite database in `database/`. This database is created automatically when the backend is initialized, and is accessed via API calls from the frontend.
7 |
8 | ## Setup
9 | - Make a copy of `.env.example`, rename it to `.env`, and fill in the fields as appropriate. `clientId` and `clientSecret` are your OAuth credentials for the Hydro API, you may request an `applicationId` on the Hydrogen Platform developer portal, and `hydroEnvironment` will remain as `Sandbox`. For more information on these parameters, see the [Raindrop SDK](https://github.com/hydrogen-dev/raindrop-sdk-js).
10 | - `npm install`
11 | - `npm start`
12 |
13 | The backend should now be listening on port 3001. You should see initialization messages for the database and the Javascript SDK for the Hydro API. If this worked, continue to frontend setup. If not, something went wrong, please open a Github issue or submit a PR.
14 |
15 | Note: If you need to use another port, edit the `start` command in `backend/package.json`, and update the `proxy` entry in `frontend/package.json`.
16 |
17 | ## Code
18 | Setup logic is contained in [app.js](./app.js). This is where the database and Raindrop SDK are initialized. Individual endpoints of the internal API are defined in [routes/](./routes/).
19 |
20 | Briefly, these endpoints are defined as follows:
21 | - `getDatabase`: returns the contents of the backend database to the frontend. Exists solely for testing purposes.
22 | - `isInDatabase`: queries the backend database to see whether or not a user has linked a hydroID.
23 | - `message`: generates a message for the user to sign and stores it in a secure session.
24 | - `registerUser`: registers an internal user's claimed HydroID with the Hydro API and logs this in the internal DB.
25 | - `unregisterUser`: unregisters an internal user's linked HydroID with the Hydro API and logs this in the internal DB.
26 | - `verifySignature`: verifies whether internal users have authenticated with the Hydro API using their linked HydroID.
27 |
28 | ## Copyright & License
29 | Copyright 2018 The Hydrogen Technology Corporation under the MIT License.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Client-Side Raindrop UI - Web
2 |
3 | This UI demo is an example implementation of the Client-Side Raindrop Authentication protocol. For a walkthrough of this process, see our [implementation guide](https://medium.com/hydrogen-api/client-side-raindrop-an-implementation-guide-4e61c84e9dda).
4 |
5 | ## Initial Setup
6 |
7 | - Download the testing Hydro mobile app: after requesting an `applicationId` through the [Hydrogen Platform developer portal](https://www.hydrogenplatform.com/login), you will be given a Testflight link to download a version of the app compatible with the `Sandbox` environment. The live Hydro mobile app on the app store **will not be compatible** with your `Sandbox` development. When you have finished developing your integration in `Sandbox`, you will need to request and be given `Production` keys, at which point you can roll the product out to your users who will be using the public-facing Hydro mobile app on the App Store.
8 |
9 | - Initialize the example backend: visit the [backend/](./backend) folder for setup instructions.
10 |
11 | - Initialize the example frontend: visit the [frontend/](./frontend) folder for setup instructions.
12 |
13 | Note: If you restart your backend without refreshing the frontend, it's possible for some session variables to get out of sync. Not to fear, simply refreshing the frontend should fix any issues!
14 |
15 | ## Terms
16 | Once your demo is live, you can begin testing out the system! It will be helpful to define some key terms:
17 |
18 | - Backend: A privileged server running code. It can receive and send data to/from the `frontend`. The two communicate via the `backend`'s internal API.
19 | - Database: The database, only accessible to code running on the backend, must store, at minimum, 3 columns: `internalUsername`, `hydroID`, `confirmed`. This codifies the following relationship: users of your site can link their account (identified by `internalUsername`, a unique identifier) with their `hydroID` (a unique identifier from the Hydro mobile app). When users first enter their `hydroID`, `confirmed` must be set to `false`, and should only be set to `true` after a user successfully signs a message (i.e. after you successfully call the `verifySignature` endpoint of the `Hydro API`).
20 | - Blockchain: Client Raindrop relies on information being stored in the `blockchain`. The `Hydro API` manages all interactions with the blockchain so you don't have to!
21 | - Frontend: The `frontend` is your client-facing website. Users must be able to opt in to Client Raindrop by providing their `hydroID`. They then should be prompted to verify this link via a first-time signature verification. Once the link is confirmed, users should be required to sign a message for every login/transaction/etc. that you wish to protect with Client Raindrop.
22 | - Raindrop SDK: A [Javascript wrapper](https://github.com/hydrogen-dev/raindrop-sdk-js) for making calls to the `Hydro API`. Abstracts away from many of the trivialities of making API calls.
23 | - Hydro API: The API that powers Client-Side Raindrop. The `backend` makes calls to the `Hydro API`, authenticated with your secret credentials.
24 |
25 |
26 | ## Copyright & License
27 | Copyright 2018 The Hydrogen Technology Corporation under the MIT License.
28 |
--------------------------------------------------------------------------------
/backend/app.js:
--------------------------------------------------------------------------------
1 | // load project-specific packages
2 | var path = require('path');
3 | require('dotenv').config(); // load Hydro API credentials
4 | var raindrop = require('@hydrogenplatform/raindrop') // load the raindrop sdk
5 | var sqlite3 = require('sqlite3'); // load our DB manager
6 | // load packages required by express
7 | var logger = require('morgan');
8 | var express = require('express');
9 | var cookieParser = require('cookie-parser');
10 | var createError = require('http-errors');
11 | var session = require('express-session')
12 | // load the routers that define endpoints in the backend's API
13 | var getDatabaseRouter = require('./routes/getDatabase');
14 | var isInDatabaseRouter = require('./routes/isInDatabase');
15 | var messageRouter = require('./routes/message');
16 | var registerUserRouter = require('./routes/registerUser');
17 | var unregisterUserRouter = require('./routes/unregisterUser');
18 | var verifySignatureRouter = require('./routes/verifySignature');
19 |
20 | var app = express();
21 |
22 | if (process.env.hydroEnvironment === undefined) {
23 | throw new Error("No configuration file loaded, is your .env file configured properly?")
24 | }
25 |
26 | // initialize client raindrop object that will wrap our calls to the Hydro API and save it in the backend's shared state
27 | var ClientRaindropPartner = new raindrop.client.RaindropPartner({
28 | environment: process.env.hydroEnvironment,
29 | clientId: process.env.clientId,
30 | clientSecret: process.env.clientSecret,
31 | applicationId: process.env.applicationId
32 | })
33 |
34 | app.set('ClientRaindropPartner', ClientRaindropPartner)
35 |
36 | console.log("Javascript SDK for the Hydro API Initialized.")
37 |
38 | // initialize database and save it in the backend's shared state
39 | var db = new sqlite3.Database(path.join('database', `myDatabase_${process.env.hydroEnvironment}.sqlite`));
40 |
41 | db.run(
42 | "CREATE TABLE IF NOT EXISTS hydro2FA " +
43 | "(internalUsername TEXT PRIMARY KEY, hydroID TEXT UNIQUE, confirmed BOOLEAN)",
44 | [], (error) => {
45 | if (error) {console.log('Database initialization failed:', error)}
46 | else {console.log('Database initialized in database/myDatabase.sqlite.'); app.set('db', db)}
47 | });
48 |
49 | // view engine setup
50 | app.set('views', path.join(__dirname, 'views'));
51 | app.set('view engine', 'jade');
52 |
53 | app.use(logger('dev'));
54 | app.use(express.json());
55 | app.use(express.urlencoded({ extended: false }));
56 | app.use(express.static(path.join(__dirname, 'public')));
57 |
58 | // initialize sessions
59 | app.use(cookieParser('hydro'));
60 |
61 | app.use(session({
62 | secret: 'hydro', // should in reality be an actual secret
63 | resave: false, // will vary per-project
64 | rolling: true,
65 | saveUninitialized: false, // will vary per-project
66 | cookie: { secure: false } // secure should be set to true in a production environment
67 | }))
68 |
69 | // register our routes
70 | app.use('/getDatabase', getDatabaseRouter);
71 | app.use('/isInDatabase', isInDatabaseRouter);
72 | app.use('/message', messageRouter);
73 | app.use('/registerUser', registerUserRouter);
74 | app.use('/unregisterUser', unregisterUserRouter);
75 | app.use('/verifySignature', verifySignatureRouter);
76 |
77 | // catch 404 and forward to error handler
78 | app.use(function(req, res, next) {
79 | next(createError(404));
80 | });
81 |
82 | // error handler
83 | app.use(function(err, req, res, next) {
84 | // set locals, only providing error in development
85 | res.locals.message = err.message;
86 | res.locals.error = req.app.get('env') === 'development' ? err : {};
87 |
88 | // render the error page
89 | res.status(err.status || 500);
90 | res.render('error');
91 | });
92 |
93 | module.exports = app;
94 |
--------------------------------------------------------------------------------
/frontend/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/frontend/src/hydroLogo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { QRCode } from 'react-qr-svg';
3 | import Toggle from 'react-toggle'
4 | import "react-toggle/style.css" // for ES6 modules
5 | import logo from './hydroLogo.svg';
6 | import './App.css';
7 |
8 | var JsonTable = require('ts-react-json-table');
9 |
10 | class App extends Component {
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | database: [{}],
15 |
16 | QREnabled: JSON.parse(sessionStorage.getItem('QREnabled')) || false,
17 | raindropEnabled: false,
18 | hydroIDConfirmed: false,
19 |
20 | internalUsername: sessionStorage.getItem('internalUsername') || 'TestUser', // for demonstration purposes only
21 | claimedHydroID: '',
22 | linkedHydroID: null,
23 |
24 | signUpStatus: '',
25 | firstTimeVerificationStatus: '',
26 | verificationStatus: '',
27 | };
28 |
29 | this.getMessage();
30 | this.getLinkedHydroID();
31 |
32 | this.registerUser = this.registerUser.bind(this);
33 | this.verify = this.verify.bind(this);
34 | this.unregisterUser = this.unregisterUser.bind(this);
35 |
36 | this.refreshDatabase = this.refreshDatabase.bind(this);
37 |
38 | this.internalUsernameChange = this.internalUsernameChange.bind(this);
39 | this.toggleQRCodes = this.toggleQRCodes.bind(this);
40 | this.claimedHydroIDChange = this.claimedHydroIDChange.bind(this);
41 | }
42 |
43 | // render the main page
44 | render() {
45 | return (
46 |
47 |
48 |
Client-Side Raindrop Demo
49 |
Enable QR codes?
52 |
53 |
54 | {this.body()}
55 |
56 |
Session Data
57 |
58 | {this.hydroIDStatus()}
59 |
60 |
Database
61 |
62 |
63 |
64 | );
65 | }
66 |
67 | // toggle QR/message display
68 | toggleQRCodes (event) {
69 | this.setState({QREnabled: event.target.checked});
70 | sessionStorage.setItem('QREnabled', JSON.stringify(event.target.checked));
71 | };
72 |
73 | // displays the appropriate html depending on whether or not the internal user has raindrop enabled or not
74 | body () {
75 | if (!this.state.raindropEnabled || !this.state.hydroIDConfirmed) {
76 | return (
77 |
78 |
First Time Sign-Up
79 |
86 |
87 |
88 | {this.state.signUpStatus}
89 |
90 |
91 |
92 | To complete your sign-up, {this.state.QREnabled ? "scan": "enter"}
93 | {' '}
94 | this code in the Hydro mobile app:
95 |