mirror of
https://github.com/filecoin-project/slate.git
synced 2024-09-11 22:07:08 +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