forked from grommet/grommet-roadmap
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Genericized, cloud storage, refactored
- Loading branch information
1 parent
a31ae42
commit ae05c5a
Showing
15 changed files
with
2,298 additions
and
1,118 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
src/**/*.md | ||
src/**/*.d.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"trailingComma": "all", | ||
"singleQuote": true, | ||
"printWidth": 80 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
const { Storage } = require('@google-cloud/storage'); | ||
const crypto = require('crypto'); | ||
|
||
const storage = new Storage(); | ||
const bucket = storage.bucket('grommet-roadmaps'); | ||
|
||
const hashPassword = (roadmap) => { | ||
if (roadmap.password) { | ||
const salt = crypto.randomBytes(16).toString('hex'); | ||
const hash = crypto.createHmac('sha512', salt); | ||
hash.update(roadmap.password); | ||
const hashedPassword = hash.digest('hex'); | ||
roadmap.password = { salt, hashedPassword }; | ||
} | ||
}; | ||
|
||
const checkPassword = (roadmap, password) => { | ||
const { salt, hashedPassword } = roadmap.password; | ||
const hash = crypto.createHmac('sha512', salt); | ||
hash.update(password); | ||
const hashed = hash.digest('hex'); | ||
return hashedPassword === hashed; | ||
}; | ||
|
||
/** | ||
* Responds to any HTTP request. | ||
* | ||
* @param {!express:Request} req HTTP request context. | ||
* @param {!express:Response} res HTTP response context. | ||
*/ | ||
exports.roadmaps = (req, res) => { | ||
res.set('Access-Control-Allow-Origin', '*'); | ||
|
||
if (req.method === 'OPTIONS') { | ||
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT'); | ||
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); | ||
res.set('Access-Control-Max-Age', '3600'); | ||
res.status(204).send(''); | ||
return; | ||
} | ||
|
||
const getPassword = () => { | ||
const authorization = req.get('Authorization'); | ||
let password; | ||
if (authorization) { | ||
const encoded = authorization.split(' ')[1]; | ||
const buffer = Buffer.from(encoded, 'base64'); | ||
password = buffer.toString(); | ||
} | ||
return password; | ||
}; | ||
|
||
if (req.method === 'GET') { | ||
const parts = req.url.split('/'); | ||
const id = decodeURIComponent(parts[1]); | ||
const password = getPassword(); | ||
|
||
// get the roadmap in question | ||
const file = bucket.file(`${id}.json`); | ||
return file | ||
.download() | ||
.then((data) => { | ||
const roadmap = JSON.parse(data[0]); | ||
if ( | ||
roadmap.password && | ||
roadmap.private && | ||
(!password || !checkPassword(roadmap, password)) | ||
) { | ||
return res.header('WWW-Authenticate', 'Basic').status(401).send(); | ||
} | ||
roadmap.id = id; | ||
delete roadmap.password; | ||
res.status(200).type('json').send(JSON.stringify(roadmap)); | ||
}) | ||
.catch((e) => res.status(400).send(e.message)); | ||
} | ||
|
||
if (req.method === 'POST') { | ||
const roadmap = req.body; | ||
const id = encodeURIComponent( | ||
`${roadmap.name.toLowerCase()}-${roadmap.email | ||
.toLowerCase() | ||
.replace('@', '-')}`.replace(/\.|\s+/g, '-'), | ||
); | ||
const file = bucket.file(`${id}.json`); | ||
const password = getPassword(); | ||
|
||
return file | ||
.download() | ||
.then((data) => { | ||
const existingRoadmap = JSON.parse(data[0]); | ||
if ( | ||
existingRoadmap.password && | ||
(!password || !checkPassword(existingRoadmap, password)) | ||
) { | ||
return res.header('WWW-Authenticate', 'Basic').status(401).send(); | ||
} | ||
|
||
hashPassword(roadmap); | ||
file | ||
.save(JSON.stringify(roadmap), { resumable: false }) | ||
.then(() => res.status(200).type('text').send(id)) | ||
.catch((e) => res.status(500).send(e.message)); | ||
}) | ||
.catch(() => { | ||
// doesn't exist yet, add it | ||
hashPassword(roadmap); | ||
file | ||
.save(JSON.stringify(roadmap), { resumable: false }) | ||
.then(() => res.status(201).type('text').send(id)) | ||
.catch((e) => res.status(500).send(e.message)); | ||
}); | ||
} | ||
|
||
if (req.method === 'PUT') { | ||
const parts = req.url.split('/'); | ||
const id = decodeURIComponent(parts[1]); | ||
const password = getPassword(); | ||
const nextRoadmap = req.body; | ||
const file = bucket.file(`${id}.json`); | ||
|
||
return file | ||
.download() | ||
.then((data) => { | ||
const roadmap = JSON.parse(data[0]); | ||
if ( | ||
roadmap.password && | ||
(!password || !checkPassword(roadmap, password)) | ||
) { | ||
return res.header('WWW-Authenticate', 'Basic').status(401).send(); | ||
} | ||
|
||
// check if the password is being changed | ||
if (typeof nextRoadmap.password === 'string') hashPassword(nextRoadmap); | ||
else nextRoadmap.password = roadmap.password; | ||
|
||
return file | ||
.save(JSON.stringify(nextRoadmap), { resumable: false }) | ||
.then(() => res.status(200).send()); | ||
}) | ||
.catch((e) => res.status(400).send(e.message)); | ||
} | ||
|
||
res.status(405).send(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"name": "roadmaps", | ||
"version": "0.0.1", | ||
"dependencies": { | ||
"@google-cloud/functions-framework": "^1.1.1", | ||
"@google-cloud/storage": "^3.0.2", | ||
"node-fetch": "^2.6.0" | ||
}, | ||
"scripts": { | ||
"start": "functions-framework --target=roadmaps" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,48 @@ | ||
import React, { useEffect, useMemo } from "react"; | ||
import { Box, Grid, Grommet, Heading } from "grommet"; | ||
import { grommet } from "grommet/themes"; | ||
import { hpe } from "grommet-theme-hpe"; | ||
import Roadmap from "./Roadmap"; | ||
import data from "./data"; | ||
|
||
const themes = { | ||
hpe: hpe, | ||
grommet: grommet, | ||
}; | ||
import React, { useEffect, useState } from 'react'; | ||
import { Box, Grid, Grommet } from 'grommet'; | ||
import { grommet } from 'grommet/themes'; | ||
import Roadmap from './Roadmap'; | ||
import Manage from './Manage'; | ||
|
||
const App = () => { | ||
const theme = useMemo(() => themes[data.theme] || themes.grommet, []); | ||
const [identifier, setIdentifier] = useState(); | ||
|
||
// load id from URL, if any | ||
useEffect(() => { | ||
document.title = data.name; | ||
}, []) | ||
const id = window.location.pathname.slice(1); | ||
setIdentifier(id ? { id } : false); | ||
}, []); | ||
|
||
return ( | ||
<Grommet full theme={theme} background="background-back"> | ||
<Grid columns={["flex", ["small", "xlarge"], "flex"]}> | ||
<Grommet full theme={grommet} background="background-back"> | ||
<Grid fill columns={['flex', ['small', 'xlarge'], 'flex']}> | ||
<Box /> | ||
<Box margin={{ horizontal: "large", bottom: 'large' }}> | ||
<Box alignSelf="center"> | ||
<Heading textAlign="center" size="small"> | ||
{data.name} | ||
</Heading> | ||
</Box> | ||
<Roadmap data={data} /> | ||
<Box margin={{ horizontal: 'large' }}> | ||
{identifier ? ( | ||
<Roadmap | ||
identifier={identifier} | ||
onClose={() => { | ||
window.history.pushState(undefined, undefined, '/'); | ||
setIdentifier(undefined); | ||
}} | ||
/> | ||
) : ( | ||
<Manage | ||
onSelect={(nextIdentifier) => { | ||
window.history.pushState( | ||
undefined, | ||
undefined, | ||
`/${nextIdentifier.id}`, | ||
); | ||
setIdentifier(nextIdentifier); | ||
}} | ||
/> | ||
)} | ||
</Box> | ||
<Box /> | ||
</Grid> | ||
</Grommet> | ||
); | ||
} | ||
}; | ||
|
||
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import React from 'react'; | ||
import { Box, Button, Form, TextInput } from 'grommet'; | ||
import { Next } from 'grommet-icons'; | ||
|
||
const Auth = ({ onChange }) => { | ||
return ( | ||
<Box fill align="center" justify="center"> | ||
<Form onSubmit={({ value: { password } }) => onChange(password)}> | ||
<Box direction="row" gap="medium"> | ||
<TextInput | ||
size="large" | ||
name="password" | ||
placeholder="password" | ||
type="password" | ||
/> | ||
<Button type="submit" icon={<Next />} hoverIndicator /> | ||
</Box> | ||
</Form> | ||
</Box> | ||
); | ||
}; | ||
|
||
export default Auth; |
Oops, something went wrong.