Skip to content

Commit

Permalink
Genericized, cloud storage, refactored
Browse files Browse the repository at this point in the history
  • Loading branch information
ericsoderberghp committed Jul 8, 2020
1 parent a31ae42 commit ae05c5a
Show file tree
Hide file tree
Showing 15 changed files with 2,298 additions and 1,118 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
node_modules
/.pnp
.pnp.js

Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src/**/*.md
src/**/*.d.ts
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 80
}
145 changes: 145 additions & 0 deletions funcs/roadmaps/index.js
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();
};
12 changes: 12 additions & 0 deletions funcs/roadmaps/package.json
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"
}
}
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"prettier": "pretty-quick --staged"
},
"pre-commit": [
"prettier"
],
"eslintConfig": {
"extends": "react-app"
},
Expand All @@ -34,5 +38,10 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"pre-commit": "^1.2.2",
"prettier": "^2.0.5",
"pretty-quick": "^2.0.1"
}
}
58 changes: 34 additions & 24 deletions src/App.js
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;
23 changes: 23 additions & 0 deletions src/Auth.js
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;
Loading

0 comments on commit ae05c5a

Please sign in to comment.