Initial commit

This commit is contained in:
@wwwjim 2020-02-18 22:30:47 -08:00
commit b8f1d42f2f
40 changed files with 1495 additions and 0 deletions

22
.babelrc Normal file
View File

@ -0,0 +1,22 @@
{
"presets": [
[
"next/babel",
{
"transform-runtime": {
"useESModules": false
}
}
]
],
"plugins": [
["emotion", {
"inline": true
}],
["module-resolver", {
"alias": {
"~": "./"
}
}]
]
}

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
.next
.env
.DS_STORE
package-lock.json
node_modules
DS_STORE
/**/*/package-lock.json
/**/*/.DS_STORE
/**/*/node_modules
/**/*/.next

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# FPS
Coming soon.

52
common/actions.js Normal file
View File

@ -0,0 +1,52 @@
import 'isomorphic-fetch';
import Cookies from 'universal-cookie';
import * as Constants from '~/common/constants';
const cookies = new Cookies();
const REQUEST_HEADERS = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
const SERVER_PATH = '';
const getHeaders = () => {
const jwt = cookies.get(Constants.session.key);
if (jwt) {
return {
...REQUEST_HEADERS,
authorization: `Bearer ${jwt}`,
};
}
return REQUEST_HEADERS;
};
export const onLocalSignIn = async (e, props, auth) => {
const options = {
method: 'POST',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify({
...auth,
}),
};
const response = await fetch(`${SERVER_PATH}/api/sign-in`, options);
const json = await response.json();
if (json.error) {
console.log(json.error);
return;
}
if (json.token) {
cookies.set(Constants.session.key, json.token);
}
window.location.href = '/sign-in-success';
};

25
common/constants.js Normal file
View File

@ -0,0 +1,25 @@
export const zindex = {
sidebar: 1,
editor: {
menu: 2,
},
};
export const session = {
key: 'WEB_SERVICE_SESSION_KEY',
};
export const colors = {
gray: '#f2f4f8',
gray2: '#dde1e6',
gray3: '#c1c7cd',
gray4: '#a2a9b0',
black: '#000000',
white: '#ffffff',
};
export const theme = {
buttonBackground: '#E5E7EA',
pageBackground: colors.gray,
pageText: colors.black,
};

8
common/credentials.js Normal file
View File

@ -0,0 +1,8 @@
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config();
}
export const CLIENT_ID = process.env.CLIENT_ID;
export const CLIENT_SECRET = process.env.CLIENT_SECRET;
export const REDIRECT_URIS = 'http://localhost:1337/sign-in-confirm';
export const JWT_SECRET = process.env.JWT_SECRET;

89
common/data.js Normal file
View File

@ -0,0 +1,89 @@
import * as Credentials from '~/common/credentials';
import * as Utilities from '~/common/utilities';
import DB from '~/db';
import JWT, { decode } from 'jsonwebtoken';
const google = require('googleapis').google;
const OAuth2 = google.auth.OAuth2;
const runQuery = async ({ queryFn, errorFn, label }) => {
let response;
try {
response = await queryFn();
} catch (e) {
response = errorFn(e);
}
console.log('[ database-query ]', { query: label });
return response;
};
export const getViewer = async (req, existingToken = undefined) => {
let viewer = {};
try {
let token = existingToken;
if (!token) {
token = Utilities.getToken(req);
}
let decode = JWT.verify(token, Credentials.JWT_SECRET);
viewer = await getUserByEmail({ email: decode.email });
} catch (e) {}
return { viewer };
};
export const getUserByEmail = async ({ email }) => {
return await runQuery({
label: 'GET_USER_BY_EMAIL',
queryFn: async () => {
const query = await DB.select('*')
.from('users')
.where({ email })
.first();
if (!query || query.error) {
return null;
}
if (query.id) {
return query;
}
return null;
},
errorFn: async e => {
return {
error: 'A new user could not be created.',
source: e,
};
},
});
};
export const createUser = async ({ email, password, salt, data = {} }) => {
return await runQuery({
label: 'createUser',
queryFn: async () => {
const query = await DB.insert({
email,
password,
salt,
data,
})
.into('users')
.returning('*');
const index = query ? query.pop() : null;
return index;
},
errorFn: async e => {
return {
error: 'A new user could not be created.',
source: e,
};
},
});
};

49
common/middleware.js Normal file
View File

@ -0,0 +1,49 @@
import * as Strings from '~/common/strings';
import * as Constants from '~/common/constants';
import * as Data from '~/common/data';
import * as Credentials from '~/common/credentials';
import JWT from 'jsonwebtoken';
export const CORS = async (req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Methods',
'GET, POST, PATCH, PUT, DELETE, OPTIONS'
);
res.header(
'Access-Control-Allow-Headers',
'Origin, Accept, Content-Type, Authorization'
);
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
next();
};
export const RequireCookieAuthentication = async (req, res, next) => {
if (Strings.isEmpty(req.headers.cookie)) {
return res.redirect('/sign-in-error');
}
const token = req.headers.cookie.replace(
/(?:(?:^|.*;\s*)WEB_SERVICE_SESSION_KEY\s*\=\s*([^;]*).*$)|^.*$/,
'$1'
);
try {
var decoded = JWT.verify(token, Credentials.JWT_SECRET);
const user = await Data.getUserByEmail({ email: decoded.email });
if (!user) {
return res.redirect('/sign-in-error');
}
} catch (err) {
console.log(err);
return res.redirect('/sign-in-error');
}
next();
};

24
common/strings.js Normal file
View File

@ -0,0 +1,24 @@
export const isEmpty = string => {
return !string || !string.toString().trim();
};
export const pluralize = (text, count) => {
return count > 1 || count === 0 ? `${text}s` : text;
};
export const elide = (string, length = 140, emptyState = '...') => {
if (isEmpty(string)) {
return emptyState;
}
if (string.length < length) {
return string.trim();
}
return `${string.substring(0, length)}...`;
};
export const toDate = data => {
const date = new Date(data);
return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`;
};

52
common/styles/global.js Normal file
View File

@ -0,0 +1,52 @@
import * as Constants from '~/common/constants';
import { injectGlobal } from 'react-emotion';
/* prettier-ignore */
export default () => injectGlobal`
@font-face {
font-family: 'mono';
src: url('/static/SFMono-Medium.woff');
}
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0;
vertical-align: baseline;
}
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
html, body {
background: ${Constants.theme.pageBackground};
color: ${Constants.theme.pageText};
font-size: 16px;
font-family: 'body', -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica,
ubuntu, roboto, noto, segoe ui, arial, sans-serif;
@media (max-width: 768px) {
font-size: 12px;
}
::-webkit-scrollbar {
display: none;
}
}
`;

22
common/utilities.js Normal file
View File

@ -0,0 +1,22 @@
import * as Strings from '~/common/strings';
// TODO(jim): Refactor this Regex so you can bind the string.
export const getToken = req => {
if (Strings.isEmpty(req.headers.cookie)) {
return null;
}
return req.headers.cookie.replace(
/(?:(?:^|.*;\s*)WEB_SERVICE_SESSION_KEY\s*\=\s*([^;]*).*$)|^.*$/,
'$1'
);
};
export const parseAuthHeader = value => {
if (typeof value !== 'string') {
return null;
}
var matches = value.match(/(\S+)\s+(\S+)/);
return matches && { scheme: matches[1], value: matches[2] };
};

115
components/Form.js Normal file
View File

@ -0,0 +1,115 @@
import * as React from 'react';
import * as Constants from '~/common/constants';
import { css } from 'react-emotion';
const STYLES_BUTTON = css`
font-weight: 700;
border: none;
margin: 0;
padding: 0 24px 0 24px;
height: 48px;
border-radius: 48px;
width: auto;
overflow: visible;
display: flex;
align-items: center;
justify-content: center;
background: ${Constants.theme.buttonBackground};
color: inherit;
font: inherit;
line-height: normal;
cursor: pointer;
white-space: nowrap;
position: relative;
text-decoration: none;
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
-webkit-appearance: none;
::-moz-focus-inner {
border: 0;
padding: 0;
}
min-width: 280px;
font-size: 18px;
`;
export const Button = ({ children, style, onClick, href }) => {
if (href) {
return (
<a className={STYLES_BUTTON} style={style} href={href}>
{children}
</a>
);
}
return (
<button className={STYLES_BUTTON} style={style} onClick={onClick}>
{children}
</button>
);
};
const STYLES_INPUT = css`
border: none;
outline: 0;
margin: 0;
padding: 0 24px 0 24px;
background: ${Constants.colors.gray3};
:focus {
border: 0;
outline: 0;
}
:active {
border: 0;
outline: 0;
}
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
-webkit-appearance: none;
::-moz-focus-inner {
border: 0;
padding: 0;
}
height: 48px;
width: 100%;
box-sizing: border-box;
font-weight: 400;
font-size: 18px;
`;
export const Input = ({
children,
style,
value,
name,
placeholder,
type = 'text',
autoComplete = 'input-autocomplete-off',
onBlur = e => {},
onFocus = e => {},
onChange = e => {},
}) => {
return (
<input
className={STYLES_INPUT}
style={style}
value={value}
onChange={onChange}
name={name}
onFocus={onFocus}
onBlur={onBlur}
type={type}
placeholder={placeholder}>
{children}
</input>
);
};

View File

@ -0,0 +1,51 @@
import * as React from 'react';
import { css } from 'react-emotion';
const STYLES_GOOGLE = css`
height: 48px;
padding: 0 24px 0 0;
border-radius: 32px;
background: #000;
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
cursor: pointer;
text-decoration: none;
transition: 200ms ease all;
transition-property: color;
:visited {
color: #fff;
}
:hover {
color: #fff;
background: #222;
}
`;
const STYLES_LOGO = css`
height: 32px;
width: 32px;
border-radius: 32px;
display: inline-flex;
background-size: cover;
background-position: 50% 50%;
background-image: url('/static/logos/google.jpg');
margin-right: 16px;
margin-left: 8px;
`;
export default class GoogleButton extends React.Component {
render() {
return (
<a className={STYLES_GOOGLE} href={this.props.href}>
<span className={STYLES_LOGO} />
Sign in with Google
</a>
);
}
}

29
components/PageState.js Normal file
View File

@ -0,0 +1,29 @@
import * as React from 'react';
import * as Constants from '~/common/constants';
import { css } from 'react-emotion';
const STYLES_PAGE_STATE = css`
font-family: 'mono';
width: 100%;
background: ${Constants.colors.black};
color: ${Constants.colors.white};
font-size: 10px;
`;
const STYLES_SECTION = css`
width: 100%;
white-space: pre-wrap;
`;
const STYLES_TITLE_SECTION = css`
padding: 8px 24px 8px 24px;
`;
export default props => {
return (
<div className={STYLES_PAGE_STATE}>
<div className={STYLES_TITLE_SECTION}>{props.children}</div>
</div>
);
};

68
components/Text.js Normal file
View File

@ -0,0 +1,68 @@
import * as React from 'react';
import * as Constants from '~/common/constants';
import { css } from 'react-emotion';
const MAX_WIDTH = 768;
const STYLES_HEADING = css`
overflow-wrap: break-word;
white-space: pre-wrap;
font-weight: 400;
font-size: 3.052rem;
position: relative;
max-width: ${MAX_WIDTH}px;
width: 100%;
padding: 0 24px 0 24px;
margin: 0 auto 0 auto;
`;
export const H1 = props => {
return <h1 {...props} className={STYLES_HEADING} />;
};
const STYLES_HEADING_TWO = css`
overflow-wrap: break-word;
white-space: pre-wrap;
font-weight: 400;
font-size: 1.728rem;
position: relative;
max-width: ${MAX_WIDTH}px;
width: 100%;
padding: 0 24px 0 24px;
margin: 0 auto 0 auto;
`;
export const H2 = props => {
return <h2 {...props} className={STYLES_HEADING_TWO} />;
};
const STYLES_PARAGRAPH = css`
overflow-wrap: break-word;
white-space: pre-wrap;
font-weight: 400;
font-size: 1.44rem;
line-height: 1.5;
position: relative;
max-width: ${MAX_WIDTH}px;
width: 100%;
padding: 0 24px 0 24px;
margin: 0 auto 0 auto;
`;
export const P = props => {
return <p {...props} className={STYLES_PARAGRAPH} />;
};
const STYLES_BODY_TEXT = css`
overflow-wrap: break-word;
white-space: pre-wrap;
font-weight: 400;
font-size: 1rem;
line-height: 1.5;
position: relative;
`;
export const BODY = props => {
return <div {...props} className={STYLES_BODY_TEXT} />;
};

13
db.js Normal file
View File

@ -0,0 +1,13 @@
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config();
}
import configs from '~/knexfile';
import knex from 'knex';
const environment =
process.env.NODE_ENV !== 'production' ? 'development' : 'production';
const envConfig = configs[environment];
const db = knex(envConfig);
module.exports = db;

6
index.js Normal file
View File

@ -0,0 +1,6 @@
require('@babel/register')({
presets: ['@babel/preset-env'],
ignore: ['node_modules', '.next'],
});
module.exports = require('./server.js');

23
knexfile.js Normal file
View File

@ -0,0 +1,23 @@
/* prettier-ignore */
module.exports = {
development: {
client: 'pg',
connection: {
port: 1334,
host: '127.0.0.1',
database: 'nptdb',
user: 'admin',
password: 'oblivion'
}
},
production: {
client: 'pg',
connection: {
port: 1334,
host: '127.0.0.1',
database: 'nptdb',
user: 'admin',
password: 'oblivion'
}
}
};

14
nodemon.json Normal file
View File

@ -0,0 +1,14 @@
{
"verbose": true,
"ignore": ["node_modules", ".next"],
"watch": [
"routes/**/*",
"common/**/*",
"components/**/*",
"pages/**/*",
"public/**/*",
"index.js",
"server.js"
],
"ext": "js json"
}

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "fps",
"version": "0.0.1",
"scripts": {
"dev": "nodemon .",
"build": "next build",
"start": "NODE_ENV=production node .",
"do-setup-database": "node ./scripts setup-database",
"do-seed-database": "node ./scripts seed-database",
"do-drop-database": "node ./scripts drop-database"
},
"dependencies": {
"@babel/preset-env": "^7.8.3",
"@babel/register": "^7.8.3",
"@loadable/component": "^5.12.0",
"babel-plugin-emotion": "^9.2.11",
"babel-plugin-module-resolver": "^4.0.0",
"bcrypt": "^3.0.8",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.4",
"dotenv": "^8.2.0",
"emotion": "^9.2.11",
"emotion-server": "^9.2.11",
"express": "^4.17.1",
"googleapis": "^47.0.0",
"isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^8.5.1",
"knex": "^0.20.10",
"next": "^9.2.2",
"pg": "^7.18.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-emotion": "^9.2.11",
"universal-cookie": "^4.0.3"
}
}

36
pages/_document.js Normal file
View File

@ -0,0 +1,36 @@
import Document, { Head, Main, NextScript } from 'next/document';
import { extractCritical } from 'emotion-server';
import injectGlobalStyles from '~/common/styles/global';
injectGlobalStyles();
export default class MyDocument extends Document {
static getInitialProps({ renderPage }) {
const page = renderPage();
const styles = extractCritical(page.html);
return { ...page, ...styles };
}
constructor(props) {
super(props);
const { __NEXT_DATA__, ids } = props;
if (ids) {
__NEXT_DATA__.ids = ids;
}
}
render() {
return (
<html>
<Head>
<style dangerouslySetInnerHTML={{ __html: this.props.css }} />
</Head>
<body>
<Main />
<NextScript />
</body>
</html>
);
}
}

37
pages/index.js Normal file
View File

@ -0,0 +1,37 @@
import Head from 'next/head';
import * as React from 'react';
import * as Strings from '~/common/strings';
import { H1 } from '~/components/Text';
import { Button } from '~/components/Form';
import { css } from 'react-emotion';
const STYLES_BODY = css`
padding: 24px;
height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
`;
const STYLES_TITLE = css`
font-size: 4.22rem;
font-weight: 600;
`;
export default class IndexPage extends React.Component {
render() {
return (
<React.Fragment>
<Head>
<title>FPS: Prototype</title>
</Head>
<div className={STYLES_BODY}>
<div className={STYLES_TITLE}>FPS: Prototype</div>
</div>
</React.Fragment>
);
}
}

50
pages/sign-in-confirm.js Normal file
View File

@ -0,0 +1,50 @@
import Head from 'next/head';
import Cookies from 'universal-cookie';
import * as React from 'react';
import * as Constants from '~/common/constants';
import { H1, H2, P } from '~/components/Text';
import { css } from 'react-emotion';
import PageState from '~/components/PageState';
const cookies = new Cookies();
const STYLES_LAYOUT = css`
padding: 24px 24px 88px 24px;
`;
function Page(props) {
React.useEffect(() => {
if (props.jwt) {
cookies.set(Constants.session.key, props.jwt);
return;
}
}, []);
return (
<React.Fragment>
<Head>
<title>FPS: Prototype</title>
</Head>
<PageState data={props} />
<div className={STYLES_LAYOUT}>
<H1 style={{ marginTop: 24 }}>Sign in confirm</H1>
<H2 style={{ marginTop: 24 }}>
<a href="/sign-in-success">View an authenticated only page.</a>
</H2>
</div>
</React.Fragment>
);
}
Page.getInitialProps = async ctx => {
return {
error: ctx.err,
viewer: ctx.query.viewer,
jwt: ctx.query.jwt,
};
};
export default Page;

45
pages/sign-in-error.js Normal file
View File

@ -0,0 +1,45 @@
import Head from 'next/head';
import Cookies from 'universal-cookie';
import * as React from 'react';
import * as Constants from '~/common/constants';
import { H1, H2, P } from '~/components/Text';
import { css } from 'react-emotion';
import PageState from '~/components/PageState';
const cookies = new Cookies();
const STYLES_LAYOUT = css`
padding: 24px 24px 88px 24px;
`;
function Page(props) {
return (
<React.Fragment>
<Head>
<title>FPS: Prototype</title>
</Head>
<PageState data={props} />
<div className={STYLES_LAYOUT}>
<H1 style={{ marginTop: 24 }}>Error</H1>
<H2 style={{ marginTop: 24 }}>
<a href="/sign-in">Sign in again.</a>
</H2>
<H2 style={{ marginTop: 24 }}>
<a href="/sign-in-success">View an authenticated only page.</a>
</H2>
</div>
</React.Fragment>
);
}
Page.getInitialProps = async ctx => {
return {
error: ctx.err,
viewer: ctx.query.viewer,
};
};
export default Page;

43
pages/sign-in-success.js Normal file
View File

@ -0,0 +1,43 @@
import Head from 'next/head';
import * as React from 'react';
import * as Constants from '~/common/constants';
import { H1, H2, P } from '~/components/Text';
import { css } from 'react-emotion';
import PageState from '~/components/PageState';
const STYLES_LAYOUT = css`
padding: 24px 24px 88px 24px;
`;
function Page(props) {
return (
<React.Fragment>
<Head>
<title>FPS: Prototype</title>
</Head>
<PageState data={props} />
<div className={STYLES_LAYOUT}>
<H1 style={{ marginTop: 24 }}>You can only see this authenticated.</H1>
<H2 style={{ marginTop: 24 }}>
<a href="/sign-in">Return to sign in page.</a>
</H2>
<H2 style={{ marginTop: 24 }}>
<a href="/sign-out">Sign out.</a>
</H2>
</div>
</React.Fragment>
);
}
Page.getInitialProps = async ctx => {
return {
error: ctx.err,
viewer: ctx.query.viewer,
data: ctx.query.data,
};
};
export default Page;

88
pages/sign-in.js Normal file
View File

@ -0,0 +1,88 @@
import Head from 'next/head';
import Cookies from 'universal-cookie';
import * as React from 'react';
import * as Actions from '~/common/actions';
import * as Constants from '~/common/constants';
import { H1, H2, P } from '~/components/Text';
import { Input, Button } from '~/components/Form';
import { css } from 'react-emotion';
import PageState from '~/components/PageState';
const STYLES_FORM = css`
padding: 24px;
width: 100%;
margin: 48px auto 0 auto;
max-width: 768px;
`;
const STYLES_TOP = css`
margin-top: 48px;
`;
const STYLES_LAYOUT = css`
padding: 24px 24px 88px 24px;
`;
function Page(props) {
const [auth, setAuth] = React.useState({ email: '', password: '' });
return (
<React.Fragment>
<Head>
<title>FPS: Prototype</title>
</Head>
<PageState data={props} />
<div className={STYLES_LAYOUT}>
<H1 style={{ marginTop: 24 }}>Sign in</H1>
<H2 style={{ marginTop: 24 }}>
<a href={props.googleURL}>Create an account through Google.</a>
</H2>
<H2 style={{ marginTop: 24 }}>
<a href="/sign-in-success">View an authenticated only page.</a>
</H2>
<H2 style={{ marginTop: 24 }}>
<a href="/sign-out">Sign out.</a>
</H2>
<div className={STYLES_FORM}>
<P style={{ marginTop: 24, padding: 0 }}>E-mail</P>
<Input
autoComplete="off"
name="email"
value={auth.email}
onChange={e =>
setAuth({ ...auth, [e.target.name]: e.target.value })
}
/>
<P style={{ marginTop: 24, padding: 0 }}>Password</P>
<Input
autoComplete="off"
name="password"
type="password"
value={auth.password}
onChange={e =>
setAuth({ ...auth, [e.target.name]: e.target.value })
}
/>
<div className={STYLES_TOP}>
<Button onClick={e => Actions.onLocalSignIn(e, props, auth)}>
Sign in or create account
</Button>
</div>
</div>
</div>
</React.Fragment>
);
}
Page.getInitialProps = async ctx => {
return {
googleURL: ctx.query.googleURL,
viewer: ctx.query.viewer,
error: ctx.err,
};
};
export default Page;

51
pages/sign-out.js Normal file
View File

@ -0,0 +1,51 @@
import Head from 'next/head';
import Cookies from 'universal-cookie';
import * as React from 'react';
import * as Constants from '~/common/constants';
import { H1, H2, P } from '~/components/Text';
import { css } from 'react-emotion';
import PageState from '~/components/PageState';
const cookies = new Cookies();
const STYLES_LAYOUT = css`
padding: 24px 24px 88px 24px;
`;
function Page(props) {
React.useEffect(() => {
const jwt = cookies.get(Constants.session.key);
if (jwt) {
cookies.remove(Constants.session.key);
return;
}
}, []);
return (
<React.Fragment>
<Head>
<title>FPS: Prototype</title>
</Head>
<PageState data={props} />
<div className={STYLES_LAYOUT}>
<H1 style={{ marginTop: 24 }}>Signed out</H1>
<H2 style={{ marginTop: 24 }}>
<a href="/sign-in">Sign in.</a>
</H2>
</div>
</React.Fragment>
);
}
Page.getInitialProps = async ctx => {
return {
error: ctx.err,
viewer: ctx.query.viewer,
jwt: ctx.query.jwt,
};
};
export default Page;

78
pages/v1/home.js Normal file
View File

@ -0,0 +1,78 @@
import Head from 'next/head';
import * as React from 'react';
import * as Constants from '~/common/constants';
import { css, styled } from 'react-emotion';
import PageState from '~/components/PageState';
const STYLES_LAYOUT_ONE = css`
font-size: 64px;
width: 320px;
height: calc(100vh - 76px);
background: ${Constants.colors.gray3};
overflow-y: scroll;
padding: 24px;
::-webkit-scrollbar {
width: 0px;
}
`;
const STYLES_LAYOUT_TWO = css`
font-size: 64px;
width: 320px;
height: calc(100vh - 76px);
background: ${Constants.colors.gray2};
overflow-y: scroll;
padding: 24px;
::-webkit-scrollbar {
width: 0px;
}
`;
const STYLES_LAYOUT_THREE = css`
min-width: 20%;
width: 100%;
background: ${Constants.colors.gray};
height: calc(100vh - 76px);
overflow-y: scroll;
padding: 24px;
::-webkit-scrollbar {
width: 0px;
}
`;
const STYLES_NAVIGATION = css`
height: 48px;
padding: 8px 24px 8px 24px;
background: ${Constants.colors.gray4};
`;
const STYLES_LAYOUT = css`
display: flex;
align-items: flex-start;
justify-content: space-between;
`;
export default class IndexPage extends React.Component {
render() {
return (
<React.Fragment>
<Head>
<title>fps: prototype: home</title>
</Head>
<PageState>FPS Prototype 0.0.1 /home</PageState>
<nav className={STYLES_NAVIGATION}>&nbsp;</nav>
<div className={STYLES_LAYOUT}>
<span className={STYLES_LAYOUT_ONE}>&nbsp;</span>
<span className={STYLES_LAYOUT_TWO}>&nbsp;</span>
<span className={STYLES_LAYOUT_THREE}>&nbsp;</span>
</div>
</React.Fragment>
);
}
}

0
public/static/.gitkeep Normal file
View File

Binary file not shown.

60
routes/api/sign-in.js Normal file
View File

@ -0,0 +1,60 @@
import * as Strings from '~/common/strings';
import * as Data from '~/common/data';
import * as Utilities from '~/common/utilities';
import * as Credentials from '~/common/credentials';
import JWT from 'jsonwebtoken';
import BCrypt from 'bcrypt';
export default async (req, res) => {
const authorization = Utilities.parseAuthHeader(req.headers.authorization);
// todo: check if a cookie already exists.
if (authorization && !Strings.isEmpty(authorization.value)) {
const verfied = JWT.verify(authorization.value, Credentials.JWT_SECRET);
return res.status(200).send({
message: 'You are already authenticated. Welcome back!',
token: authorization.value,
});
}
if (Strings.isEmpty(req.body.email)) {
return res
.status(500)
.send({ error: 'An e-mail address was not provided.' });
}
if (Strings.isEmpty(req.body.password)) {
return res.status(500).send({ error: 'A password was not provided.' });
}
let user = await Data.getUserByEmail({ email: req.body.email });
if (!user) {
const salt = BCrypt.genSaltSync(10);
const hash = BCrypt.hashSync(req.body.password, salt);
const double = BCrypt.hashSync(hash, salt);
const triple = BCrypt.hashSync(double, process.env.PASSWORD_SECRET);
user = await Data.createUser({
email: req.body.email,
password: triple,
salt,
});
} else {
const phaseOne = BCrypt.hashSync(req.body.password, user.salt);
const phaseTwo = BCrypt.hashSync(phaseOne, user.salt);
const phaseThree = BCrypt.hashSync(phaseTwo, process.env.PASSWORD_SECRET);
if (phaseThree !== user.password) {
return res.status(500).send({ error: 'We could not authenticate you.' });
}
}
const token = JWT.sign(
{ user: user.id, email: user.email },
Credentials.JWT_SECRET
);
return res.status(200).send({ token });
};

14
routes/index.js Normal file
View File

@ -0,0 +1,14 @@
import signIn from '~/routes/sign-in';
import signInConfirm from '~/routes/sign-in-confirm';
import signInSuccess from '~/routes/sign-in-success';
import apiSignIn from '~/routes/api/sign-in';
module.exports = {
signIn,
signInConfirm,
signInSuccess,
api: {
signIn: apiSignIn,
},
};

71
routes/sign-in-confirm.js Normal file
View File

@ -0,0 +1,71 @@
import * as Credentials from '~/common/credentials';
import * as Data from '~/common/data';
import JWT from 'jsonwebtoken';
import BCrypt from 'bcrypt';
const google = require('googleapis').google;
const OAuth2 = google.auth.OAuth2;
export default async (req, res, app) => {
const client = new OAuth2(
Credentials.CLIENT_ID,
Credentials.CLIENT_SECRET,
Credentials.REDIRECT_URIS
);
if (req.query.error) {
return res.redirect('/sign-in-error');
}
client.getToken(req.query.code, async (error, token) => {
if (error) {
return res.redirect('/sign-in-error');
}
const jwt = JWT.sign(token, Credentials.JWT_SECRET);
const client = new OAuth2(
Credentials.CLIENT_ID,
Credentials.CLIENT_SECRET,
Credentials.REDIRECT_URIS
);
client.credentials = JWT.verify(jwt, Credentials.JWT_SECRET);
const people = google.people({
version: 'v1',
auth: client,
});
const response = await people.people.get({
resourceName: 'people/me',
personFields: 'emailAddresses,names,organizations,memberships',
});
const email = response.data.emailAddresses[0].value;
const name = response.data.names[0].displayName;
const password = BCrypt.genSaltSync(10);
let user = await Data.getUserByEmail({ email });
if (!user) {
const salt = BCrypt.genSaltSync(10);
const hash = BCrypt.hashSync(password, salt);
const double = BCrypt.hashSync(hash, salt);
const triple = BCrypt.hashSync(double, process.env.PASSWORD_SECRET);
user = await Data.createUser({
email,
password: triple,
salt,
data: { name },
});
}
const authToken = JWT.sign(
{ user: user.id, email: user.email },
Credentials.JWT_SECRET
);
app.render(req, res, '/sign-in-confirm', { jwt: authToken, viewer: user });
});
};

View File

@ -0,0 +1,7 @@
import * as Data from '~/common/data';
export default async (req, res, app) => {
const { viewer } = await Data.getViewer(req);
return app.render(req, res, '/sign-in-success', { viewer });
};

27
routes/sign-in.js Normal file
View File

@ -0,0 +1,27 @@
import * as Credentials from '~/common/credentials';
import * as Data from '~/common/data';
const google = require('googleapis').google;
const OAuth2 = google.auth.OAuth2;
export default async (req, res, app) => {
const client = new OAuth2(
Credentials.CLIENT_ID,
Credentials.CLIENT_SECRET,
Credentials.REDIRECT_URIS
);
const googleURL = client.generateAuthUrl({
access_type: 'offline',
scope: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/user.organization.read',
],
prompt: 'consent',
});
const { viewer } = await Data.getViewer(req);
app.render(req, res, '/', { googleURL, viewer });
};

26
scripts/drop-database.js Normal file
View File

@ -0,0 +1,26 @@
import configs from '~/knexfile';
import knex from 'knex';
const environment =
process.env.NODE_ENV !== 'local-production' ? 'development' : 'production';
const envConfig = configs[environment];
console.log(`SETUP: database`, envConfig);
const db = knex(envConfig);
console.log(`RUNNING: drop-database.js NODE_ENV=${environment}`);
// --------------------------
// SCRIPTS
// --------------------------
const dropUserTable = db.schema.dropTable('users');
// --------------------------
// RUN
// --------------------------
Promise.all([dropUserTable]);
console.log(`FINISHED: drop-database.js NODE_ENV=${environment}`);

6
scripts/index.js Normal file
View File

@ -0,0 +1,6 @@
require('@babel/register')({
presets: ['@babel/preset-env'],
ignore: ['node_modules', '.next'],
});
module.exports = require('./' + process.argv[2] + '.js');

52
scripts/seed-database.js Normal file
View File

@ -0,0 +1,52 @@
import configs from '~/knexfile';
import knex from 'knex';
const environment =
process.env.NODE_ENV !== 'local-production' ? 'development' : 'production';
const envConfig = configs[environment];
console.log(`SETUP: database`, envConfig);
const db = knex(envConfig);
console.log(`RUNNING: seed-database.js NODE_ENV=${environment}`);
// --------------------------
// SCRIPTS
// --------------------------
const createUserTable = db.schema.createTable('users', function(table) {
table
.uuid('id')
.primary()
.unique()
.notNullable()
.defaultTo(db.raw('uuid_generate_v4()'));
table
.timestamp('created_at')
.notNullable()
.defaultTo(db.raw('now()'));
table
.timestamp('updated_at')
.notNullable()
.defaultTo(db.raw('now()'));
table
.string('email')
.unique()
.notNullable();
table.string('password').nullable();
table.string('salt').nullable();
table.jsonb('data').nullable();
});
// --------------------------
// RUN
// --------------------------
Promise.all([createUserTable]);
console.log(`FINISHED: seed-database.js NODE_ENV=${environment}`);

16
scripts/setup-database.js Normal file
View File

@ -0,0 +1,16 @@
import configs from '~/knexfile';
import knex from 'knex';
const environment =
process.env.NODE_ENV !== 'local-production' ? 'development' : 'production';
const envConfig = configs[environment];
console.log(`SETUP: database`, envConfig);
const db = knex(envConfig);
console.log(`RUNNING: setup-database.js NODE_ENV=${environment}`);
Promise.all([db.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')]);
console.log(`FINISHED: setup-database.js NODE_ENV=${environment}`);

72
server.js Normal file
View File

@ -0,0 +1,72 @@
import * as Middleware from '~/common/middleware';
import * as Data from '~/common/data';
import * as Routes from '~/routes';
import express from 'express';
import next from 'next';
import bodyParser from 'body-parser';
import compression from 'compression';
const dev = process.env.NODE_ENV !== 'production';
const port = process.env.PORT || 1337;
const app = next({ dev, quiet: false });
const nextRequestHandler = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
if (!dev) {
server.use(compression());
}
server.use(Middleware.CORS);
server.use('/public', express.static('public'));
server.use(bodyParser.json());
server.use(
bodyParser.urlencoded({
extended: false,
})
);
server.post('/api/sign-in', async (req, res) => {
return await Routes.api.signIn(req, res);
});
server.get('/', async (req, res) => {
return await Routes.signIn(req, res, app);
});
server.get('/sign-in-confirm', async (req, res) => {
return await Routes.signInConfirm(req, res, app);
});
server.get(
'/sign-in-success',
Middleware.RequireCookieAuthentication,
async (req, res) => {
return await Routes.signInSuccess(req, res, app);
}
);
server.get('/sign-in-error', async (req, res) => {
const { viewer } = await Data.getViewer(req);
app.render(req, res, '/sign-in-error', { viewer });
});
server.get('/sign-out', async (req, res) => {
const { viewer } = await Data.getViewer(req);
app.render(req, res, '/sign-out', { viewer });
});
server.get('*', async (req, res) => {
return nextRequestHandler(req, res, req.url);
});
server.listen(port, err => {
if (err) {
throw err;
}
console.log(`[ filecoin pinning server ] http://localhost:${port}`);
});
});