mirror of
https://github.com/filecoin-project/slate.git
synced 2024-12-22 16:41:38 +03:00
Initial commit
This commit is contained in:
commit
b8f1d42f2f
22
.babelrc
Normal file
22
.babelrc
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"next/babel",
|
||||||
|
{
|
||||||
|
"transform-runtime": {
|
||||||
|
"useESModules": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
["emotion", {
|
||||||
|
"inline": true
|
||||||
|
}],
|
||||||
|
["module-resolver", {
|
||||||
|
"alias": {
|
||||||
|
"~": "./"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
}
|
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.next
|
||||||
|
.env
|
||||||
|
.DS_STORE
|
||||||
|
package-lock.json
|
||||||
|
node_modules
|
||||||
|
DS_STORE
|
||||||
|
|
||||||
|
/**/*/package-lock.json
|
||||||
|
/**/*/.DS_STORE
|
||||||
|
/**/*/node_modules
|
||||||
|
/**/*/.next
|
||||||
|
|
52
common/actions.js
Normal file
52
common/actions.js
Normal 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
25
common/constants.js
Normal 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
8
common/credentials.js
Normal 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
89
common/data.js
Normal 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
49
common/middleware.js
Normal 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
24
common/strings.js
Normal 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
52
common/styles/global.js
Normal 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
22
common/utilities.js
Normal 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
115
components/Form.js
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
51
components/GoogleButton.js
Normal file
51
components/GoogleButton.js
Normal 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
29
components/PageState.js
Normal 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
68
components/Text.js
Normal 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
13
db.js
Normal 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
6
index.js
Normal 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
23
knexfile.js
Normal 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
14
nodemon.json
Normal 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
37
package.json
Normal 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
36
pages/_document.js
Normal 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
37
pages/index.js
Normal 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
50
pages/sign-in-confirm.js
Normal 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
45
pages/sign-in-error.js
Normal 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
43
pages/sign-in-success.js
Normal 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
88
pages/sign-in.js
Normal 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
51
pages/sign-out.js
Normal 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
78
pages/v1/home.js
Normal 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}> </nav>
|
||||||
|
<div className={STYLES_LAYOUT}>
|
||||||
|
<span className={STYLES_LAYOUT_ONE}> </span>
|
||||||
|
<span className={STYLES_LAYOUT_TWO}> </span>
|
||||||
|
<span className={STYLES_LAYOUT_THREE}> </span>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
0
public/static/.gitkeep
Normal file
0
public/static/.gitkeep
Normal file
BIN
public/static/SFMono-Medium.woff
Normal file
BIN
public/static/SFMono-Medium.woff
Normal file
Binary file not shown.
60
routes/api/sign-in.js
Normal file
60
routes/api/sign-in.js
Normal 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
14
routes/index.js
Normal 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
71
routes/sign-in-confirm.js
Normal 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 });
|
||||||
|
});
|
||||||
|
};
|
7
routes/sign-in-success.js
Normal file
7
routes/sign-in-success.js
Normal 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
27
routes/sign-in.js
Normal 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
26
scripts/drop-database.js
Normal 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
6
scripts/index.js
Normal 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
52
scripts/seed-database.js
Normal 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
16
scripts/setup-database.js
Normal 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
72
server.js
Normal 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}`);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user