first commit
This commit is contained in:
commit
521512e9a3
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 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*
|
||||||
|
old
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
trailingComma: "es5"
|
||||||
|
tabWidth: 2
|
||||||
|
semi: false
|
||||||
|
singleQuote: true
|
13487
package-lock.json
generated
Normal file
13487
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "blockchain-stories",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.19.0",
|
||||||
|
"bootstrap": "^4.3.1",
|
||||||
|
"concurrently": "^4.1.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"morgan": "^1.9.1",
|
||||||
|
"pg": "^7.11.0",
|
||||||
|
"react": "^16.8.6",
|
||||||
|
"react-bootstrap": "^1.0.0-beta.9",
|
||||||
|
"react-datetime-picker": "^2.6.0",
|
||||||
|
"react-dom": "^16.8.6",
|
||||||
|
"react-scripts": "3.0.1",
|
||||||
|
"sequelize": "^5.10.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "concurrently --kill-others-on-fail \"node server\" \"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 159 KiB |
17
public/index.html
Normal file
17
public/index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
|
||||||
|
<title>app</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
public/manifest.json
Normal file
15
public/manifest.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"short_name": "app",
|
||||||
|
"name": "app",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.png",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
35
server/api/blockchain.js
Normal file
35
server/api/blockchain.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const router = require('express').Router()
|
||||||
|
const { Blockchain } = require('../db/models')
|
||||||
|
module.exports = router
|
||||||
|
|
||||||
|
router.get('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const hashes = await Blockchain.findAll()
|
||||||
|
|
||||||
|
res.status(201).json(hashes)
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get('/:hash', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const hash = await Blockchain.findOne({
|
||||||
|
where: { hashedStory: req.params.hashedStory },
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(201).json(hash)
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const hash = await Blockchain.create(req.body)
|
||||||
|
|
||||||
|
res.status(201).json(hash)
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
11
server/api/index.js
Executable file
11
server/api/index.js
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
const router = require('express').Router()
|
||||||
|
module.exports = router
|
||||||
|
|
||||||
|
router.use('/stories', require('./stories'))
|
||||||
|
router.use('/blockchain', require('./blockchain'))
|
||||||
|
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
const error = new Error('Not Found!!!!!!!')
|
||||||
|
error.status = 404
|
||||||
|
next(error)
|
||||||
|
})
|
32
server/api/stories.js
Executable file
32
server/api/stories.js
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
const router = require('express').Router()
|
||||||
|
const { Story } = require('../db/models')
|
||||||
|
module.exports = router
|
||||||
|
|
||||||
|
router.post('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const story = await Story.create(req.body)
|
||||||
|
|
||||||
|
res.json(story)
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post('/read', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const story = await Story.find({
|
||||||
|
where: {
|
||||||
|
hash: req.body.hash,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (story) {
|
||||||
|
res.json(story)
|
||||||
|
} else {
|
||||||
|
console.log('no story found.')
|
||||||
|
res.status(204).send()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
18
server/db/db.js
Executable file
18
server/db/db.js
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
const Sequelize = require('sequelize')
|
||||||
|
const pkg = require('../../package.json')
|
||||||
|
|
||||||
|
const databaseName = pkg.name
|
||||||
|
|
||||||
|
const createDB = () => {
|
||||||
|
const db = new Sequelize(
|
||||||
|
process.env.DATABASE_URL || `postgres://localhost:5432/${databaseName}`,
|
||||||
|
{
|
||||||
|
logging: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = createDB()
|
||||||
|
|
||||||
|
module.exports = db
|
6
server/db/index.js
Executable file
6
server/db/index.js
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
const db = require("./db")
|
||||||
|
|
||||||
|
// register models
|
||||||
|
require("./models")
|
||||||
|
|
||||||
|
module.exports = db
|
10
server/db/models/blockchain.js
Normal file
10
server/db/models/blockchain.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const Sequelize = require('sequelize')
|
||||||
|
const db = require('../db')
|
||||||
|
|
||||||
|
const Blockchain = db.define('blockchain', {
|
||||||
|
hashedStory: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = Blockchain
|
4
server/db/models/index.js
Executable file
4
server/db/models/index.js
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
const Story = require('./story')
|
||||||
|
const Blockchain = require('./blockchain')
|
||||||
|
|
||||||
|
module.exports = { Story, Blockchain }
|
16
server/db/models/story.js
Executable file
16
server/db/models/story.js
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
const Sequelize = require('sequelize')
|
||||||
|
const db = require('../db')
|
||||||
|
|
||||||
|
const Story = db.define('story', {
|
||||||
|
dateOfEvent: {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
},
|
||||||
|
hashedStory: {
|
||||||
|
type: Sequelize.TEXT,
|
||||||
|
},
|
||||||
|
encryptedStory: {
|
||||||
|
type: Sequelize.TEXT,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = Story
|
28
server/db/seed.js
Executable file
28
server/db/seed.js
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
const db = require('../db')
|
||||||
|
const { Story, Blockchain } = require('./models')
|
||||||
|
|
||||||
|
const h1 = {
|
||||||
|
hashedStory: 'sasdvasd',
|
||||||
|
}
|
||||||
|
|
||||||
|
const s1 = { hashedStory: 'sasdvasd', encryptedStory: 'sdsdf' }
|
||||||
|
|
||||||
|
async function runSeed() {
|
||||||
|
await db.sync({ force: true })
|
||||||
|
console.log('db synced!')
|
||||||
|
console.log('seeding...')
|
||||||
|
try {
|
||||||
|
await Blockchain.create(h1)
|
||||||
|
await Story.create(s1)
|
||||||
|
console.log(`seeded successfully`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
process.exitCode = 1
|
||||||
|
} finally {
|
||||||
|
console.log('closing db connection')
|
||||||
|
await db.close()
|
||||||
|
console.log('db connection closed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runSeed()
|
26
server/index.js
Executable file
26
server/index.js
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const path = require('path')
|
||||||
|
const app = express()
|
||||||
|
const morgan = require('morgan')
|
||||||
|
const port = process.env.PORT || 1337
|
||||||
|
const cors = require('cors')
|
||||||
|
|
||||||
|
app.use(morgan('tiny'))
|
||||||
|
app.use(cors())
|
||||||
|
// body parsing middleware
|
||||||
|
app.use(express.json())
|
||||||
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
app.use(require('body-parser').text())
|
||||||
|
app.use('/api', require('./api'))
|
||||||
|
|
||||||
|
app.get('*', (req, res) =>
|
||||||
|
res.sendFile(path.resolve(__dirname, '..', 'public', 'index.html'))
|
||||||
|
)
|
||||||
|
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err)
|
||||||
|
console.error(err.stack)
|
||||||
|
res.status(err.status || 500).send(err.message || 'Internal server error.')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(port, () => console.log(`Doin' haxor stuff on port ${port}`))
|
144
src/App.js
Normal file
144
src/App.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Navbar,
|
||||||
|
Footer,
|
||||||
|
Home,
|
||||||
|
CreateStory,
|
||||||
|
RetrieveStory,
|
||||||
|
HashedStories,
|
||||||
|
} from './components'
|
||||||
|
import { Container } from 'react-bootstrap'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import {
|
||||||
|
encryptString,
|
||||||
|
decryptString,
|
||||||
|
generatePassword,
|
||||||
|
createHash,
|
||||||
|
} from './encrypt'
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
loading: true,
|
||||||
|
component: 'home',
|
||||||
|
hashedStories: [],
|
||||||
|
encryptedStories: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const API = 'http://localhost:1337'
|
||||||
|
|
||||||
|
class App extends React.Component {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.state = initialState
|
||||||
|
this.navigate = this.navigate.bind(this)
|
||||||
|
this.submitStory = this.submitStory.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchBlockchain() {
|
||||||
|
const { data } = await axios.get(API + '/api/blockchain')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
async fetchData() {
|
||||||
|
try {
|
||||||
|
const hashedStories = await this.fetchBlockchain()
|
||||||
|
this.setState({ hashedStories })
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ error: e.message })
|
||||||
|
} finally {
|
||||||
|
this.setState({ loading: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async submitStory(storyData) {
|
||||||
|
const { eventDetails } = storyData
|
||||||
|
|
||||||
|
const encryptionKey = generatePassword()
|
||||||
|
const encryptedStory = encryptString(eventDetails, encryptionKey)
|
||||||
|
const hashedStory = createHash(encryptedStory)
|
||||||
|
try {
|
||||||
|
await axios.post(API + '/api/stories', {
|
||||||
|
hashedStory,
|
||||||
|
encryptedStory,
|
||||||
|
})
|
||||||
|
await axios.post(API + '/api/blockchain', {
|
||||||
|
hashedStory,
|
||||||
|
})
|
||||||
|
this.setState({
|
||||||
|
encryptionKey,
|
||||||
|
hashedStory,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ error: e.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchStory(encryptionKey, hash) {
|
||||||
|
try {
|
||||||
|
const retrievedStory = await axios.get(`/api/stories/${hash}`)
|
||||||
|
|
||||||
|
const decryptedStory = decryptString(
|
||||||
|
retrievedStory.encryptedStory,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
return this.setState({ decryptedStory })
|
||||||
|
} catch (e) {
|
||||||
|
return this.setState({ retrievalError: 'Story not found' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(component) {
|
||||||
|
this.setState({ component })
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoading() {
|
||||||
|
return <div>loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
renderError() {
|
||||||
|
return <div>something went wrong.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMain() {
|
||||||
|
if (this.state.component === 'home') return <Home {...this.state} />
|
||||||
|
if (this.state.component === 'create')
|
||||||
|
return (
|
||||||
|
<CreateStory
|
||||||
|
navigate={this.navigate}
|
||||||
|
submitStory={this.submitStory}
|
||||||
|
{...this.state}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
if (this.state.component === 'read')
|
||||||
|
return (
|
||||||
|
<RetrieveStory
|
||||||
|
navigate={this.navigate}
|
||||||
|
fetchStory={this.fetchStory}
|
||||||
|
{...this.state}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
if (this.state.component === 'hashed')
|
||||||
|
return <HashedStories navigate={this.navigate} {...this.state} />
|
||||||
|
return <div>something else happened.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
renderApp() {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Navbar navigate={this.navigate} />
|
||||||
|
{this.renderMain()}
|
||||||
|
<Footer />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.loading) return this.renderLoading()
|
||||||
|
if (this.state.error) return this.renderError()
|
||||||
|
return this.renderApp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
9
src/__tests__/App.test.js
Normal file
9
src/__tests__/App.test.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
ReactDOM.render(<App />, div)
|
||||||
|
ReactDOM.unmountComponentAtNode(div)
|
||||||
|
})
|
72
src/components/CreateStory/CreateStoryForm.js
Normal file
72
src/components/CreateStory/CreateStoryForm.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import DateTimePicker from 'react-datetime-picker'
|
||||||
|
|
||||||
|
//----------------------------------------------//
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
dateOfEvent: new Date(),
|
||||||
|
eventDetails: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
class StoryForm extends React.Component {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.state = initialState
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this)
|
||||||
|
this.handleChange = this.handleChange.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(e) {
|
||||||
|
console.log(this.state)
|
||||||
|
this.setState({ [e.target.name]: e.target.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSubmit(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.props.submitStory({ ...this.state })
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const { eventDetails } = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5 mb-5">
|
||||||
|
{this.state.error ? <div>something went wrong.</div> : <div />}
|
||||||
|
|
||||||
|
<form onSubmit={this.handleSubmit}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="dateOfEvent" className="col-form-label">
|
||||||
|
Date of Event
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<DateTimePicker
|
||||||
|
className="mb-4 form-control"
|
||||||
|
id="dateOfEvent"
|
||||||
|
name="dateOfEvent"
|
||||||
|
/>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="eventDetails" className="col-form-label">
|
||||||
|
Event Details
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
className="form-control"
|
||||||
|
type="text"
|
||||||
|
rows="5"
|
||||||
|
id="eventDetails"
|
||||||
|
name="eventDetails"
|
||||||
|
onChange={this.handleChange}
|
||||||
|
value={eventDetails}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------//
|
||||||
|
|
||||||
|
export default StoryForm
|
48
src/components/CreateStory/StorySubmittedView.js
Normal file
48
src/components/CreateStory/StorySubmittedView.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
//----------------------------------------------//
|
||||||
|
|
||||||
|
const StorySubmittedView = props => {
|
||||||
|
const { encryptionKey, hashedStory } = props
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<div className="form-group row">
|
||||||
|
<label htmlFor="password" className="col-sm-3 col-form-label">
|
||||||
|
Encryption Key:
|
||||||
|
</label>
|
||||||
|
<div className="col-sm-7">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
className="form-control-plaintext"
|
||||||
|
id="encryptionKey"
|
||||||
|
value={encryptionKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-group row">
|
||||||
|
<label htmlFor="hash" className="col-sm-3 col-form-label">
|
||||||
|
Hash:
|
||||||
|
</label>
|
||||||
|
<div className="col-sm-7">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
className="form-control-plaintext"
|
||||||
|
id="hash"
|
||||||
|
value={hashedStory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-group row">
|
||||||
|
<div className="col">
|
||||||
|
Don't lose these or your data will be unable to be decrypted.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------//
|
||||||
|
|
||||||
|
export default StorySubmittedView
|
10
src/components/CreateStory/index.js
Normal file
10
src/components/CreateStory/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import CreateStoryForm from './CreateStoryForm'
|
||||||
|
import StorySubmittedView from './StorySubmittedView'
|
||||||
|
|
||||||
|
const ReadStory = props => {
|
||||||
|
if (props.hashedStory) return <StorySubmittedView {...props} />
|
||||||
|
return <CreateStoryForm {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReadStory
|
11
src/components/Footer.js
Normal file
11
src/components/Footer.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
return (
|
||||||
|
<div className="footer bg-dark">
|
||||||
|
<p className="text-center text-white">Copyright © 2018</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer
|
25
src/components/HashedStories/index.js
Normal file
25
src/components/HashedStories/index.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Table } from 'react-bootstrap'
|
||||||
|
|
||||||
|
const HashedStories = props => {
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Story Hash</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{props.hashedStories.map(hs => (
|
||||||
|
<tr key={hs.id}>
|
||||||
|
<td>{hs.id}</td>
|
||||||
|
<td>{hs.hashedStory}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HashedStories
|
21
src/components/Home.js
Normal file
21
src/components/Home.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Home = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Welcome to Stories. </h3>
|
||||||
|
<p>
|
||||||
|
To submit a story on the "Add" page, enter your name and details. All of
|
||||||
|
your data will be encrypted before being sent to the server using an
|
||||||
|
automatically-generated password, which will be given to you, along with
|
||||||
|
the SHA256 hash value of the encrypted story.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When you're ready to retrieve your story, you will need the password and
|
||||||
|
the hash value. Without these, the data are unable to be retrieved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
49
src/components/Navbar.js
Normal file
49
src/components/Navbar.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Navbar, Nav } from 'react-bootstrap'
|
||||||
|
|
||||||
|
const AppNavbar = props => {
|
||||||
|
return (
|
||||||
|
<Navbar variant="dark" bg="dark" expand="lg">
|
||||||
|
<Navbar.Brand href="#home">Stories</Navbar.Brand>
|
||||||
|
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||||
|
<Navbar.Collapse id="basic-navbar-nav">
|
||||||
|
<Nav className="mr-auto">
|
||||||
|
<Nav.Link
|
||||||
|
href="#home"
|
||||||
|
onClick={() => {
|
||||||
|
props.navigate('home')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Nav.Link>
|
||||||
|
<Nav.Link
|
||||||
|
href="#create"
|
||||||
|
onClick={() => {
|
||||||
|
props.navigate('create')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Story
|
||||||
|
</Nav.Link>
|
||||||
|
<Nav.Link
|
||||||
|
href="#read"
|
||||||
|
onClick={() => {
|
||||||
|
props.navigate('read')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retrieve Story
|
||||||
|
</Nav.Link>
|
||||||
|
<Nav.Link
|
||||||
|
href="#hashed"
|
||||||
|
onClick={() => {
|
||||||
|
props.navigate('hashed')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
See Hashes
|
||||||
|
</Nav.Link>
|
||||||
|
</Nav>
|
||||||
|
</Navbar.Collapse>
|
||||||
|
</Navbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppNavbar
|
12
src/components/RetrieveStory/ReadStoryView.js
Normal file
12
src/components/RetrieveStory/ReadStoryView.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const ReadStoryView = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3> {props.eventDate}</h3>
|
||||||
|
<p>{props.eventDetails}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReadStoryView
|
87
src/components/RetrieveStory/RetrieveStoryForm.js
Normal file
87
src/components/RetrieveStory/RetrieveStoryForm.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
//----------------------------------------------//
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
password: '',
|
||||||
|
hash: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
class RetrieveStoryForm extends React.Component {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.state = initialState
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this)
|
||||||
|
this.handleChange = this.handleChange.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(e) {
|
||||||
|
this.setState({ [e.target.name]: e.target.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
try {
|
||||||
|
const encryptedStory = this.props.fetchStory(
|
||||||
|
this.state.password,
|
||||||
|
this.state.hash
|
||||||
|
)
|
||||||
|
this.props.navigate('read')
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="mt-3 mb-5">
|
||||||
|
<h3> Retrieve A Story</h3>
|
||||||
|
<p>Enter your secret key and your hash below.</p>
|
||||||
|
<form onSubmit={this.handleSubmit}>
|
||||||
|
<div className="form-group row">
|
||||||
|
<label htmlFor="password" className="col-sm-3 col-form-label">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="col-sm-7">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Password"
|
||||||
|
onChange={this.handleChange}
|
||||||
|
value={this.state.password}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-group row">
|
||||||
|
<label htmlFor="hash" className="col-sm-3 col-form-label">
|
||||||
|
Hash:
|
||||||
|
</label>
|
||||||
|
<div className="col-sm-7">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="hash"
|
||||||
|
name="hash"
|
||||||
|
placeholder="Hash"
|
||||||
|
onChange={this.handleChange}
|
||||||
|
value={this.state.hash}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-group row">
|
||||||
|
<div className="col-sm-3">
|
||||||
|
<button className="btn btn-dark " type="submit">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//----------------------------------------------//
|
||||||
|
|
||||||
|
export default RetrieveStoryForm
|
10
src/components/RetrieveStory/index.js
Normal file
10
src/components/RetrieveStory/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReadStoryView from './ReadStoryView'
|
||||||
|
import RetrieveStoryForm from './RetrieveStoryForm'
|
||||||
|
|
||||||
|
const ReadStory = props => {
|
||||||
|
if (props.retrievedStory) return <ReadStoryView {...props} />
|
||||||
|
return <RetrieveStoryForm {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReadStory
|
7
src/components/index.js
Normal file
7
src/components/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export { default as Navbar } from './Navbar'
|
||||||
|
export { default as Footer } from './Footer'
|
||||||
|
export { default as Home } from './Home'
|
||||||
|
|
||||||
|
export { default as CreateStory } from './CreateStory'
|
||||||
|
export { default as HashedStories } from './HashedStories'
|
||||||
|
export { default as RetrieveStory } from './RetrieveStory'
|
7
src/encrypt/createHash.js
Normal file
7
src/encrypt/createHash.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const crypto = require('crypto')
|
||||||
|
|
||||||
|
export default plainText =>
|
||||||
|
crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(plainText)
|
||||||
|
.digest('base64')
|
51
src/encrypt/decryptString.js
Normal file
51
src/encrypt/decryptString.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const ALGORITHM_NAME = 'aes-128-gcm'
|
||||||
|
const ALGORITHM_NONCE_SIZE = 12
|
||||||
|
const ALGORITHM_TAG_SIZE = 16
|
||||||
|
const ALGORITHM_KEY_SIZE = 16
|
||||||
|
const PBKDF2_NAME = 'sha256'
|
||||||
|
const PBKDF2_SALT_SIZE = 16
|
||||||
|
const PBKDF2_ITERATIONS = 32767
|
||||||
|
|
||||||
|
export default function decryptString(
|
||||||
|
base64CiphertextAndNonceAndSalt,
|
||||||
|
password
|
||||||
|
) {
|
||||||
|
// Decode the base64.
|
||||||
|
let ciphertextAndNonceAndSalt = new Buffer(
|
||||||
|
base64CiphertextAndNonceAndSalt,
|
||||||
|
'base64'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create buffers of salt and ciphertextAndNonce.
|
||||||
|
let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE)
|
||||||
|
let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE)
|
||||||
|
|
||||||
|
// Derive the key using PBKDF2.
|
||||||
|
let key = crypto.pbkdf2Sync(
|
||||||
|
new Buffer(password, 'utf8'),
|
||||||
|
salt,
|
||||||
|
PBKDF2_ITERATIONS,
|
||||||
|
ALGORITHM_KEY_SIZE,
|
||||||
|
PBKDF2_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decrypt and return result.
|
||||||
|
return decrypt(ciphertextAndNonce, key).toString('utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrypt(ciphertextAndNonce, key) {
|
||||||
|
// Create buffers of nonce, ciphertext and tag.
|
||||||
|
let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE)
|
||||||
|
let ciphertext = ciphertextAndNonce.slice(
|
||||||
|
ALGORITHM_NONCE_SIZE,
|
||||||
|
ciphertextAndNonce.length - ALGORITHM_TAG_SIZE
|
||||||
|
)
|
||||||
|
let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE)
|
||||||
|
|
||||||
|
// Create the cipher instance.
|
||||||
|
let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce)
|
||||||
|
|
||||||
|
// Decrypt and return result.
|
||||||
|
cipher.setAuthTag(tag)
|
||||||
|
return Buffer.concat([cipher.update(ciphertext), cipher.final()])
|
||||||
|
}
|
44
src/encrypt/encryptString.js
Normal file
44
src/encrypt/encryptString.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
const crypto = require('crypto')
|
||||||
|
|
||||||
|
const ALGORITHM_NAME = 'aes-128-gcm'
|
||||||
|
const ALGORITHM_NONCE_SIZE = 12
|
||||||
|
const ALGORITHM_KEY_SIZE = 16
|
||||||
|
const PBKDF2_NAME = 'sha256'
|
||||||
|
const PBKDF2_SALT_SIZE = 16
|
||||||
|
const PBKDF2_ITERATIONS = 32767
|
||||||
|
|
||||||
|
export default function encryptString(plaintext, password) {
|
||||||
|
// Generate a 128-bit salt using a CSPRNG.
|
||||||
|
let salt = crypto.randomBytes(PBKDF2_SALT_SIZE)
|
||||||
|
|
||||||
|
// Derive a key using PBKDF2.
|
||||||
|
let key = crypto.pbkdf2Sync(
|
||||||
|
new Buffer(password, 'utf8'),
|
||||||
|
salt,
|
||||||
|
PBKDF2_ITERATIONS,
|
||||||
|
ALGORITHM_KEY_SIZE,
|
||||||
|
PBKDF2_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypt and prepend salt.
|
||||||
|
let ciphertextAndNonceAndSalt = Buffer.concat([
|
||||||
|
salt,
|
||||||
|
encrypt(new Buffer(plaintext, 'utf8'), key),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Return as base64 string.
|
||||||
|
return ciphertextAndNonceAndSalt.toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
function encrypt(plaintext, key) {
|
||||||
|
// Generate a 96-bit nonce using a CSPRNG.
|
||||||
|
let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE)
|
||||||
|
|
||||||
|
// Create the cipher instance.
|
||||||
|
let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce)
|
||||||
|
|
||||||
|
// Encrypt and prepend nonce.
|
||||||
|
let ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()])
|
||||||
|
|
||||||
|
return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()])
|
||||||
|
}
|
15
src/encrypt/generatePassword.js
Normal file
15
src/encrypt/generatePassword.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const MIN = 10
|
||||||
|
const MAX = 20
|
||||||
|
const ALL_CHARS =
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(){}:,.<>?'
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
let key = ''
|
||||||
|
const passwordLength = Math.floor(Math.random() * (MAX - MIN + 1) + MIN)
|
||||||
|
for (let i = 0; i < passwordLength; i++) {
|
||||||
|
key += ALL_CHARS.charAt(Math.floor(Math.random() * ALL_CHARS.length))
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
export default generatePassword
|
5
src/encrypt/generateSalt.js
Normal file
5
src/encrypt/generateSalt.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function generateSalt() {
|
||||||
|
// Generate a 128-bit salt using a CSPRNG.
|
||||||
|
return crypto.randomBytes(PBKDF2_SALT_SIZE)
|
||||||
|
}
|
||||||
|
export default generateSalt
|
30
src/encrypt/hash-story.js
Normal file
30
src/encrypt/hash-story.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const story = {
|
||||||
|
dateOfEvent: '12345',
|
||||||
|
eventDetails: `Finding duplicate records
|
||||||
|
Main article: Hash table
|
||||||
|
When storing records in a large unsorted file, one may use a hash function to map each record to an index into a table T, and to collect in each bucket T[i] a list of the numbers of all records with the same hash value i. Once the table is complete, any two duplicate records will end up in the same bucket. The duplicates can then be found by scanning every bucket T[i] which contains two or more members, fetching those records, and comparing them. With a table of appropriate size, this method is likely to be much faster than any alternative approach (such as sorting the file and comparing all consecutive pairs).
|
||||||
|
|
||||||
|
Protecting data
|
||||||
|
Main article: Security of cryptographic hash functions
|
||||||
|
A hash value can be used to uniquely identify secret information. This requires that the hash function is collision-resistant, which means that it is very hard to find data that will generate the same hash value. These functions are categorized into cryptographic hash functions and provably secure hash functions. Functions in the second category are the most secure but also too slow for most practical purposes. Collision resistance is accomplished in part by generating very large hash values. For example, SHA-2, one of the most widely used cryptographic hash functions, generates 256-bit values.
|
||||||
|
|
||||||
|
Finding similar records
|
||||||
|
Main article: Locality sensitive hashing
|
||||||
|
Hash functions can also be used to locate table records whose key is similar, but not identical, to a given key; or pairs of records in a large file which have similar keys. For that purpose, one needs a hash function that maps similar keys to hash values that differ by at most m, where m is a small integer (say, 1 or 2). If one builds a table T of all record numbers, using such a hash function, then similar records will end up in the same bucket, or in nearby buckets. Then one need only check the records in each bucket T[i] against those in buckets T[i+k] where k ranges between −m and m.
|
||||||
|
|
||||||
|
This class includes the so-called acoustic fingerprint algorithms, that are used to locate similar-sounding entries in large collection of audio files. For this application, the hash function must be as insensitive as possible to data capture or transmission errors, and to trivial changes such as timing and volume changes, compression, etc.[3]
|
||||||
|
|
||||||
|
Finding similar substrings
|
||||||
|
The same techniques can be used to find equal or similar stretches in a large collection of strings, such as a document repository or a genomic database. In this case, the input strings are broken into many small pieces, and a hash function is used to detect potentially equal pieces, as above.
|
||||||
|
|
||||||
|
The Rabin–Karp algorithm is a relatively fast string searching algorithm that works in O(n) time on average. It is based on the use of hashing to compare strings.
|
||||||
|
|
||||||
|
Geometric hashing
|
||||||
|
This principle is widely used in computer graphics, computational geometry and many other disciplines, to solve many proximity problems in the plane or in three-dimensional space, such as finding closest pairs in a set of points, similar shapes in a list of shapes, similar images in an image database, and so on. In these applications, the set of all inputs is some sort of metric space, and the hashing function can be interpreted as a partition of that space into a grid of cells. The table is often an array with two or more indices (called a grid file, grid index, bucket grid, and similar names), and the hash function returns an index tuple. This special case of hashing is known as geometric hashing or the grid method. Geometric hashing is also used in telecommunications (usually under the name vector quantization) to encode and compress multi-dimensional signals.
|
||||||
|
|
||||||
|
Standard uses of hashing in cryptography
|
||||||
|
Main article: Cryptographic hash function
|
||||||
|
Some standard applications that employ hash functions include authentication, message integrity (using an HMAC (Hashed MAC)), message fingerprinting, data corruption detection, and digital signature efficiency.
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
export default story
|
5
src/encrypt/index.js
Normal file
5
src/encrypt/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { default as encryptString } from './encryptString'
|
||||||
|
export { default as decryptString } from './decryptString'
|
||||||
|
export { default as generatePassword } from './generatePassword'
|
||||||
|
export { default as createHash } from './createHash'
|
||||||
|
export { default as testStory } from './hash-story'
|
10
src/index.js
Normal file
10
src/index.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import 'bootstrap/dist/css/bootstrap.css'
|
||||||
|
|
||||||
|
import App from './App'
|
||||||
|
import * as serviceWorker from './serviceWorker'
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById('app'))
|
||||||
|
|
||||||
|
serviceWorker.unregister()
|
135
src/serviceWorker.js
Normal file
135
src/serviceWorker.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// This optional code is used to register a service worker.
|
||||||
|
// register() is not called by default.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||||
|
// resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||||
|
// opt-in, read https://bit.ly/CRA-PWA
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === 'localhost' ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === '[::1]' ||
|
||||||
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(
|
||||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function register(config) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
'This web app is being served cache-first by a service ' +
|
||||||
|
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl, config) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
'New content is available and will be used when all ' +
|
||||||
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl, config) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl)
|
||||||
|
.then(response => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(
|
||||||
|
'No internet connection found. App is running in offline mode.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user