postgres: user accounts, migration scripts, api route rewrite, stopping point

This commit is contained in:
@wwwjim 2020-07-17 03:24:20 -07:00
parent ee9e8fd27d
commit b6a8c880cd
23 changed files with 471 additions and 494 deletions

View File

@ -16,7 +16,10 @@ Working on Slate requires an internet connection because we are using a hosted P
### .env ### .env
You will need to create a file called `.env`. Never commit your credentials to the repository. **You don't need this file if you only work on the design system.** - We use a `dotenv` file to manage sensitive values and secrets.
- You must create this file to work on the application.
- You don't need to create a `.env` file if you're only working on the design system.
- There will be no local data in the short term.
``` ```
POSTGRES_ADMIN_PASSWORD=XXX POSTGRES_ADMIN_PASSWORD=XXX

View File

@ -8,25 +8,8 @@ const REQUEST_HEADERS = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
const dev = process.env.NODE_ENV !== "production"; const dev = process.env.NODE_ENV !== "www";
const SERVER_PATH = dev ? "http://localhost:1337" : "https://slate.host";
const SERVER_PATH = dev
? "http://localhost:1337"
: "https://filecoin.onrender.com";
export const rehydrateViewer = async () => {
const options = {
method: "POST",
headers: REQUEST_HEADERS,
credentials: "include",
body: JSON.stringify({}),
};
const response = await fetch(`${SERVER_PATH}/_/viewer`, options);
const json = await response.json();
return json;
};
export const setDefaultConfig = async (data) => { export const setDefaultConfig = async (data) => {
const options = { const options = {
@ -85,3 +68,58 @@ export const sendWalletAddressFilecoin = async (data) => {
return json; return json;
}; };
// NOTE(jim):
// New WWW Requests.
export const hydrateAuthenticatedUser = async (data) => {
const options = {
method: "POST",
headers: REQUEST_HEADERS,
credentials: "include",
body: JSON.stringify({ data }),
};
const response = await fetch(`${SERVER_PATH}/api/hydrate`, options);
const json = await response.json();
return json;
};
export const deleteUser = async (data) => {
const options = {
method: "DELETE",
headers: REQUEST_HEADERS,
credentials: "include",
body: JSON.stringify({ data }),
};
const response = await fetch(`${SERVER_PATH}/api/users/delete`, options);
const json = await response.json();
return json;
};
export const createUser = async (data) => {
const options = {
method: "POST",
headers: REQUEST_HEADERS,
credentials: "include",
body: JSON.stringify({ data }),
};
const response = await fetch(`${SERVER_PATH}/api/users/create`, options);
const json = await response.json();
return json;
};
export const health = async (data) => {
const options = {
method: "POST",
headers: REQUEST_HEADERS,
credentials: "include",
body: JSON.stringify({ data: { success: true } }),
};
const response = await fetch(`${SERVER_PATH}/api/_`, options);
const json = await response.json();
return json;
};

View File

@ -1,8 +1,3 @@
if (process.env.NODE_ENV !== "www") {
console.log("[ prototype ] loading dotenv");
require("dotenv").config();
}
module.exports = { module.exports = {
development: { development: {
client: "pg", client: "pg",

View File

@ -0,0 +1,6 @@
import createUser from "~/node_common/data/methods/create-user";
import updateUserById from "~/node_common/data/methods/update-user-by-id";
import deleteUserByUsername from "~/node_common/data/methods/delete-user-by-username";
import getUserByUsername from "~/node_common/data/methods/get-user-by-username";
export { createUser, updateUserById, deleteUserByUsername, getUserByUsername };

View File

@ -0,0 +1,27 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ email, password, username, salt, data = {} }) => {
return await runQuery({
label: "CREATE_USER",
queryFn: async (DB) => {
const query = await DB.insert({
email,
password,
salt,
data,
username,
})
.into("users")
.returning("*");
const index = query ? query.pop() : null;
return index;
},
errorFn: async (e) => {
return {
error: "CREATE_USER",
source: e,
};
},
});
};

View File

@ -0,0 +1,20 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ username }) => {
return await runQuery({
label: "DELETE_USER_BY_USERNAME",
queryFn: async (DB) => {
const data = await DB.from("users")
.where({ username })
.del();
return 1 === data;
},
errorFn: async (e) => {
return {
error: "DELETE_USER_BY_USERNAME",
source: e,
};
},
});
};

View File

@ -0,0 +1,29 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ username }) => {
return await runQuery({
label: "GET_USER_BY_USERNAME",
queryFn: async (DB) => {
const query = await DB.select("*")
.from("users")
.where({ username })
.first();
if (!query || query.error) {
return null;
}
if (query.id) {
return query;
}
return null;
},
errorFn: async (e) => {
return {
error: "GET_USER_BY_USERNAME",
source: e,
};
},
});
};

View File

@ -0,0 +1,26 @@
import { runQuery } from "~/node_common/data/utilities";
export default async ({ id, data }) => {
return await runQuery({
label: "UPDATE_USER_BY_ID",
queryFn: async (DB) => {
const data = await DB.from("users")
.where("id", o.id)
.update({
data: {
...data,
},
})
.returning("*");
const index = data ? data.pop() : null;
return index;
},
errorFn: async (e) => {
return {
error: "UPDATE_USER",
source: e,
};
},
});
};

View File

@ -0,0 +1,13 @@
import DB from "~/node_common/database";
export const runQuery = async ({ queryFn, errorFn, label }) => {
let response;
try {
response = await queryFn(DB);
} catch (e) {
response = errorFn(e);
}
console.log("[ database-query ]", { query: label });
return response;
};

View File

@ -6,6 +6,6 @@ import configs from "~/knexfile";
import knex from "knex"; import knex from "knex";
const envConfig = configs["development"]; const envConfig = configs["development"];
const db = knex(envConfig); const Database = knex(envConfig);
module.exports = db; export default Database;

View File

@ -1,9 +1,23 @@
export const init = (middleware) => {
return (req, res) =>
new Promise((resolve, reject) => {
middleware(req, res, (result) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
};
export const CORS = async (req, res, next) => { export const CORS = async (req, res, next) => {
res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Origin", "*");
res.header( res.header(
"Access-Control-Allow-Methods", "Access-Control-Allow-Methods",
"GET, POST, PATCH, PUT, DELETE, OPTIONS" "GET, POST, PATCH, PUT, DELETE, OPTIONS"
); );
res.header( res.header(
"Access-Control-Allow-Headers", "Access-Control-Allow-Headers",
"Origin, Accept, Content-Type, Authorization" "Origin, Accept, Content-Type, Authorization"

9
node_common/powergate.js Normal file
View File

@ -0,0 +1,9 @@
import * as Constants from "~/node_common/constants";
import { createPow } from "@textile/powergate-client";
// NOTE(jim):
// https://github.com/textileio/js-powergate-client
const Powergate = createPow({ host: Constants.POWERGATE_HOST });
export default Powergate;

View File

@ -1,34 +1,5 @@
import * as Constants from "./constants"; import * as Constants from "./constants";
import FS from "fs-extra";
export const resetFileSystem = async () => {
console.log("[ prototype ] deleting old token and library data ");
if (FS.existsSync(`./.data`)) {
FS.removeSync("./.data", { recursive: true });
}
console.log("[ prototype ] deleting old avatar data ");
if (FS.existsSync(Constants.AVATAR_STORAGE_URL)) {
FS.removeSync(Constants.AVATAR_STORAGE_URL, { recursive: true });
}
console.log("[ prototype ] deleting old file data ");
if (FS.existsSync(Constants.FILE_STORAGE_URL)) {
FS.removeSync(Constants.FILE_STORAGE_URL, { recursive: true });
}
console.log("[ prototype ] creating new avatar folder ");
FS.mkdirSync(Constants.AVATAR_STORAGE_URL, { recursive: true });
FS.writeFileSync(`${Constants.AVATAR_STORAGE_URL}.gitkeep`, "");
console.log("[ prototype ] creating new local file folder ");
FS.mkdirSync(Constants.FILE_STORAGE_URL, { recursive: true });
FS.writeFileSync(`${Constants.FILE_STORAGE_URL}.gitkeep`, "");
return true;
};
// NOTE(jim): Data that does not require a Powergate token. // NOTE(jim): Data that does not require a Powergate token.
export const refresh = async ({ PG }) => { export const refresh = async ({ PG }) => {
const Health = await PG.health.check(); const Health = await PG.health.check();

View File

@ -7,7 +7,7 @@
"node": ">=11 <12" "node": ">=11 <12"
}, },
"scripts": { "scripts": {
"dev": "node . --unhandled-rejections=strict", "dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 node . --unhandled-rejections=strict",
"start": "NODE_ENV=www node . --unhandled-rejections=strict", "start": "NODE_ENV=www node . --unhandled-rejections=strict",
"build-delete": "rm -rf .next && rm -rf dist/mac", "build-delete": "rm -rf .next && rm -rf dist/mac",
"build": "NODE_ENV=www next build", "build": "NODE_ENV=www next build",
@ -44,7 +44,9 @@
"@emotion/server": "11.0.0-next.12", "@emotion/server": "11.0.0-next.12",
"@textile/hub": "^0.3.4", "@textile/hub": "^0.3.4",
"@textile/powergate-client": "0.1.0-beta.14", "@textile/powergate-client": "0.1.0-beta.14",
"@textile/threads-core": "^0.1.32",
"babel-plugin-module-resolver": "^4.0.0", "babel-plugin-module-resolver": "^4.0.0",
"bcrypt": "^5.0.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"chart.js": "^2.9.3", "chart.js": "^2.9.3",
"chartkick": "^3.2.0", "chartkick": "^3.2.0",
@ -54,6 +56,7 @@
"formidable": "^1.2.2", "formidable": "^1.2.2",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^8.5.1",
"knex": "^0.20.10", "knex": "^0.20.10",
"moment": "^2.27.0", "moment": "^2.27.0",
"next": "^9.4.4", "next": "^9.4.4",
@ -65,8 +68,7 @@
"react-tippy": "^1.3.4", "react-tippy": "^1.3.4",
"regenerator-runtime": "^0.13.5", "regenerator-runtime": "^0.13.5",
"three": "^0.108.0", "three": "^0.108.0",
"uuid": "^8.0.0", "uuid": "^8.0.0"
"ws": "^7.3.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-transform-runtime": "^7.10.4", "@babel/plugin-transform-runtime": "^7.10.4",

13
pages/api/_.js Normal file
View File

@ -0,0 +1,13 @@
import * as MW from "~/node_common/middleware";
import DB from "~/node_common/database";
const initCORS = MW.init(MW.CORS);
export default (req, res) => {
initCORS(req, res);
return res
.status(200)
.json({ decorator: "SERVER_HEALTH_CHECK", data: req.body.data });
};

71
pages/api/hydrate.js Normal file
View File

@ -0,0 +1,71 @@
import * as MW from "~/node_common/middleware";
import * as Utilities from "~/node_common/utilities";
import * as Constants from "~/node_common/constants";
import * as Data from "~/node_common/data";
import { Buckets, UserAuth } from "@textile/hub";
import { Libp2pCryptoIdentity } from "@textile/threads-core";
import PG from "~/node_common/powergate";
const initCORS = MW.init(MW.CORS);
export default async (req, res) => {
initCORS(req, res);
const user = await Data.getUserByUsername({
username: req.body.data.username,
});
if (!user) {
return res.status(200).json({ decorator: "SERVER_HYDRATE", error: true });
}
if (user.error) {
return res.status(200).json({ decorator: "SERVER_HYDRATE", error: true });
}
const identity = await Libp2pCryptoIdentity.fromString(user.data.tokens.api);
const b = Buckets.withUserAuth(identity);
// TODO(jim): Bug on the server:
// const buckets = await b.list();
let data = {
peersList: null,
messageList: null,
status: null,
addrsList: null,
info: null,
state: null,
local: {
photo: null,
name: `node`,
settings_deals_auto_approve: false,
},
library: [
{
...Utilities.createFolder({ id: Constants.FILE_STORAGE_URL }),
file: "Files",
name: "Files",
},
],
buckets: [],
};
PG.setToken(user.data.tokens.pg);
const updates = await Utilities.refresh({ PG });
const updatesWithToken = await Utilities.refreshWithToken({
PG,
});
data = await Utilities.updateStateData(data, {
...updates,
...updatesWithToken,
});
return res
.status(200)
.send({ decorator: "SERVER_HYDRATE", success: true, data });
};

72
pages/api/users/create.js Normal file
View File

@ -0,0 +1,72 @@
import * as MW from "~/node_common/middleware";
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import PG from "~/node_common/powergate";
import JWT from "jsonwebtoken";
import BCrypt from "bcrypt";
import { Libp2pCryptoIdentity } from "@textile/threads-core";
const initCORS = MW.init(MW.CORS);
const SECRET = `$2b$13$${process.env.LOCAL_PASSWORD_SECRET}`;
export default async (req, res) => {
initCORS(req, res);
if (Strings.isEmpty(req.body.data.email)) {
return res
.status(500)
.send({ error: "An e-mail address was not provided." });
}
if (Strings.isEmpty(req.body.data.password)) {
return res.status(500).send({ error: "A password was not provided." });
}
const salt = await BCrypt.genSalt(13);
console.log({ salt });
const hash = await BCrypt.hash(req.body.data.password, salt);
console.log({ hash });
const double = await BCrypt.hash(hash, salt);
console.log({
double,
});
console.log(SECRET);
const triple = await BCrypt.hash(double, SECRET);
console.log({ triple });
const FFS = await PG.ffs.create();
const pg = FFS.token ? FFS.token : null;
// API
const identity = await Libp2pCryptoIdentity.fromRandom();
const api = identity.toString();
const user = await Data.createUser({
email: req.body.data.email,
password: triple,
salt,
username: req.body.data.username,
data: { tokens: { pg, api } },
});
if (!user) {
return res
.status(200)
.json({ decorator: "SERVER_USER_CREATE", error: true });
}
if (user.error) {
return res
.status(200)
.json({ decorator: "SERVER_USER_CREATE", error: true });
}
return res.status(200).json({
decorator: "SERVER_USER_CREATE",
user: { username: user.username },
});
};

20
pages/api/users/delete.js Normal file
View File

@ -0,0 +1,20 @@
import * as MW from "~/node_common/middleware";
import * as Data from "~/node_common/data";
const initCORS = MW.init(MW.CORS);
export default async (req, res) => {
initCORS(req, res);
const deleted = await Data.deleteUserByUsername({
username: req.body.data.username,
});
if (!deleted) {
return res
.status(200)
.json({ decorator: "SERVER_USER_DELETE", error: true });
}
return res.status(200).json({ decorator: "SERVER_USER_DELETE", deleted });
};

11
pages/api/users/update.js Normal file
View File

@ -0,0 +1,11 @@
import * as MW from "~/node_common/middleware";
import DB from "~/node_common/database";
const initCORS = MW.init(MW.CORS);
export default async (req, res) => {
initCORS(req, res);
return res.status(200).json({ decorator: "SERVER_USER_UPDATE" });
};

View File

@ -65,10 +65,8 @@ const getCurrentNavigationStateById = (navigation, targetId) => {
}; };
export const getServerSideProps = async (context) => { export const getServerSideProps = async (context) => {
const data = await Actions.rehydrateViewer();
return { return {
props: { ...context.query, ...data.data }, props: { ...context.query },
}; };
}; };
@ -79,24 +77,47 @@ export default class ApplicationPage extends React.Component {
history: [{ id: 1, scrollTop: 0 }], history: [{ id: 1, scrollTop: 0 }],
currentIndex: 0, currentIndex: 0,
data: null, data: null,
selected: State.getSelectedState(this.props), selected: null,
viewer: State.getInitialState(this.props), viewer: null,
sidebar: null, sidebar: null,
file: null, file: null,
}; };
componentDidMount() { async componentDidMount() {
this._socket = new WebSocket(`ws://localhost:${this.props.wsPort}`); if (this.props.production) {
this._socket.onmessage = (m) => { console.log("Disabled application in production setting for now.");
console.log(m); return null;
if (m.type === "message") { }
const parsed = JSON.parse(m.data);
if (parsed.action === "UPDATE_VIEWER") { let response = await Actions.deleteUser({
this.rehydrate({ data: parsed.data }); username: "test",
} });
}
}; console.log(response);
response = await Actions.createUser({
email: "test@test.com",
password: "test",
username: "test",
});
if (response.error) {
console.log("Could not create a new user");
return null;
}
console.log(response);
response = await Actions.hydrateAuthenticatedUser({
username: "test",
});
console.log(response);
this.setState({
viewer: State.getInitialState(response.data),
selected: State.getSelectedState(response.data),
});
window.addEventListener("dragenter", this._handleDragEnter); window.addEventListener("dragenter", this._handleDragEnter);
window.addEventListener("dragleave", this._handleDragLeave); window.addEventListener("dragleave", this._handleDragLeave);
@ -327,6 +348,11 @@ export default class ApplicationPage extends React.Component {
}; };
render() { render() {
// TODO(jim): Render Sign In Screen.
if (!this.state.viewer) {
return null;
}
const navigation = NavigationData.generate(this.state.viewer.library); const navigation = NavigationData.generate(this.state.viewer.library);
const next = this.state.history[this.state.currentIndex]; const next = this.state.history[this.state.currentIndex];
const current = getCurrentNavigationStateById(navigation, next.id); const current = getCurrentNavigationStateById(navigation, next.id);

View File

@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import * as Constants from "~/common/constants"; import * as Constants from "~/common/constants";
import * as Actions from "~/common/actions";
import { css } from "@emotion/react"; import { css } from "@emotion/react";
@ -18,6 +19,10 @@ const STYLES_HEADING = css`
font-size: 2.88rem; font-size: 2.88rem;
line-height: 1.5; line-height: 1.5;
color: ${Constants.system.black}; color: ${Constants.system.black};
@media (max-width: 768px) {
font-size: 1rem;
}
`; `;
const STYLES_PARAGRAPH = css` const STYLES_PARAGRAPH = css`
@ -25,6 +30,10 @@ const STYLES_PARAGRAPH = css`
font-size: 2.88rem; font-size: 2.88rem;
line-height: 1.5; line-height: 1.5;
color: ${Constants.system.pitchBlack}; color: ${Constants.system.pitchBlack};
@media (max-width: 768px) {
font-size: 1rem;
}
`; `;
export const getServerSideProps = async (context) => { export const getServerSideProps = async (context) => {
@ -34,6 +43,11 @@ export const getServerSideProps = async (context) => {
}; };
export default class IndexPage extends React.Component { export default class IndexPage extends React.Component {
async componentDidMount() {
const response = await Actions.health();
console.log(response);
}
render() { render() {
const title = `Slate`; const title = `Slate`;
const description = const description =
@ -54,9 +68,7 @@ export default class IndexPage extends React.Component {
<a href="https://filecoin.io/">Filecoin</a>. <a href="https://filecoin.io/">Filecoin</a>.
<br /> <br />
<br /> <br />
{!this.props.hide ? ( <a href="/application">Test Application (Local Only)</a>
<a href="/application">Test Application</a>
) : null}
<br /> <br />
<a href="/system">View Design System</a> <a href="/system">View Design System</a>
</p> </p>

View File

@ -1,3 +1,8 @@
if (process.env.NODE_ENV !== "www") {
console.log("[ slate ] loading dotenv");
require("dotenv").config();
}
require("@babel/register")({ require("@babel/register")({
presets: ["@babel/preset-env"], presets: ["@babel/preset-env"],
ignore: ["node_modules", ".next"], ignore: ["node_modules", ".next"],

426
server.js
View File

@ -1,455 +1,49 @@
if (process.env.NODE_ENV !== "www") { if (process.env.NODE_ENV !== "www") {
console.log("[ prototype ] loading dotenv");
require("dotenv").config(); require("dotenv").config();
} }
import * as Middleware from "./common/middleware";
import * as Strings from "./common/strings"; import * as Strings from "./common/strings";
import * as Middleware from "./node_common/middleware";
import * as Utilities from "./node_common/utilities"; import * as Utilities from "./node_common/utilities";
import * as Constants from "./node_common/constants"; import * as Constants from "./node_common/constants";
import * as Database from "./node_common/database";
import { createPow, ffs } from "@textile/powergate-client";
// NOTE(jim):
// https://github.com/textileio/js-powergate-client
const PowerGate = createPow({ host: Constants.POWERGATE_HOST });
import { v4 as uuid } from "uuid";
// TODO(jim):
// CUT THIS DURING THE POSTGRES MOVE.
import FS from "fs-extra";
import WebSocketServer from "ws";
import express from "express"; import express from "express";
import formidable from "formidable";
import next from "next"; import next from "next";
import bodyParser from "body-parser";
import compression from "compression"; import compression from "compression";
import path from "path";
// TODO(jim):
// CUT THIS DURING THE POSTGRES MOVE.
let client = null;
let state = null;
const production = const production =
process.env.NODE_ENV === "production" || process.env.NODE_ENV === "www"; process.env.NODE_ENV === "production" || process.env.NODE_ENV === "www";
const productionWeb = process.env.NODE_ENV === "www"; const productionWeb = process.env.NODE_ENV === "www";
const port = process.env.PORT || 1337; const port = process.env.PORT || 1337;
const wsPort = process.env.WS_PORT || 2448;
const resetData = process.env.npm_config_reset_data;
const app = next({ dev: !production, dir: __dirname, quiet: false }); const app = next({ dev: !production, dir: __dirname, quiet: false });
const nextRequestHandler = app.getRequestHandler(); const handler = app.getRequestHandler();
const setIntervalViewerUpdatesUnsafe = async () => {
if (client) {
try {
console.log("[ prototype ] polling: new viewer state.");
state = await Utilities.emitState({
state,
client,
PG: PowerGate,
});
console.log("[ prototype ] polling: new library state.");
state = await Utilities.refreshLibrary({
state,
PG: PowerGate,
FFS: ffs,
});
} catch (e) {
console.log(e);
}
}
setTimeout(setIntervalViewerUpdatesUnsafe, Constants.POLLING_RATE);
};
app.prepare().then(async () => { app.prepare().then(async () => {
console.log("[ prototype ] initializing ");
// TODO(jim):
// CUT THIS DURING THE POSTGRES MOVE.
state = {
production,
port,
wsPort,
token: null,
library: null,
status: null,
messageList: null,
peersList: null,
addrsList: null,
info: null,
local: null,
};
try {
// TODO(jim):
// CUT THIS DURING THE POSTGRES MOVE.
// NOTE(daniel): Wipe all of the local data when --reset-data flag is added to npm run dev.
if (resetData) {
await Utilities.resetFileSystem();
}
const updates = await Utilities.refresh({ PG: PowerGate });
state = await Utilities.updateStateData(state, updates);
console.log("[ prototype ] updated without token");
// NOTE(jim): This is a configuration folder with all of the client tokens.
// TODO(jim): Unnecessary if we use a local and remote postgres.
const dirnameData = path.join(__dirname, "/.data");
if (!FS.existsSync(dirnameData)) {
FS.mkdirSync(dirnameData, { recursive: true });
}
// NOTE(jim): This will create a token for authentication with powergate.
// TODO(jim): Roll this up into Postgres instead.
const dirnamePowergate = path.join(__dirname, "/.data/powergate-token");
if (!FS.existsSync(dirnamePowergate)) {
const FFS = await PowerGate.ffs.create();
state.token = FFS.token ? FFS.token : null;
// NOTE(jim): Write a new token file.
if (state.token) {
FS.writeFileSync(dirnamePowergate, state.token);
}
} else {
state.token = FS.readFileSync(dirnamePowergate, "utf8");
}
if (state.token) {
console.log("[ prototype ] powergate token:", state.token);
PowerGate.setToken(state.token);
}
const tokenUpdates = await Utilities.refreshWithToken({
PG: PowerGate,
});
state = await Utilities.updateStateData(state, tokenUpdates);
console.log("[ prototype ] updated with token");
// NOTE(jim): Local library retrieval or creation
// TODO(jim): Needs to support nested folders in the future.
// TODO(jim): May consider a move to buckets.
const dirnameLibrary = path.join(__dirname, "/.data/library.json");
if (!FS.existsSync(dirnameLibrary)) {
const librarySchema = {
library: [
{
...Utilities.createFolder({ id: Constants.FILE_STORAGE_URL }),
file: "Files",
name: "Files",
},
],
};
FS.writeFileSync(dirnameLibrary, JSON.stringify(librarySchema));
state.library = librarySchema.library;
} else {
const parsedLibrary = FS.readFileSync(dirnameLibrary, "utf8");
state.library = JSON.parse(parsedLibrary).library;
}
// NOTE(jim): Local settings retrieval or creation
// TODO(jim): Move this to postgres later.
const dirnameLocalSettings = path.join(
__dirname,
"/.data/local-settings.json"
);
if (!FS.existsSync(dirnameLocalSettings)) {
const localSettingsSchema = {
local: {
photo: null,
name: `node-${uuid()}`,
settings_deals_auto_approve: false,
},
};
FS.writeFileSync(
dirnameLocalSettings,
JSON.stringify(localSettingsSchema)
);
state.local = localSettingsSchema.local;
} else {
const parsedLocal = FS.readFileSync(dirnameLocalSettings, "utf8");
state.local = JSON.parse(parsedLocal).local;
}
} catch (e) {
console.log(e);
console.log('[ prototype ] "/" -- WILL REDIRECT TO /SYSTEM ');
console.log(
"[ prototype ] SLATE WILL NOT RUN LOCALLY UNTIL YOU HAVE "
);
console.log("[ prototype ] PROPERLY CONFIGURED POWERGATE AND ");
console.log(
"[ prototype ] CONNECTED TO THE FILECOIN NETWORK (DEVNET/TESTNET) "
);
}
const server = express(); const server = express();
// TODO(jim): Temporarily disable web sockets for web production
// since we have no web version of Slate yet.
if (!productionWeb) {
const WSS = new WebSocketServer.Server({ port: wsPort });
WSS.on("connection", (s) => {
// TODO(jim): Suppport more than one client.
client = s;
s.on("close", function() {
s.send(JSON.stringify({ action: null, data: "closed" }));
});
s.send(JSON.stringify({ action: null, data: "connected" }));
});
}
if (productionWeb) { if (productionWeb) {
server.use(compression()); server.use(compression());
} }
server.use(Middleware.CORS);
server.use("/public", express.static("public")); server.use("/public", express.static("public"));
server.use(bodyParser.json());
server.use(
bodyParser.urlencoded({
extended: false,
})
);
server.post("/_/viewer", async (req, res) => {
let data = state;
if (!productionWeb) {
const updates = await Utilities.refresh({ PG: PowerGate });
const updatesWithToken = await Utilities.refreshWithToken({
PG: PowerGate,
});
data = await Utilities.updateStateData(data, {
...updates,
...updatesWithToken,
});
}
return res.status(200).send({ success: true, data });
});
server.post("/_/deals/storage", async (req, res) => {
if (Strings.isEmpty(req.body.src)) {
return res.status(500).send({ success: false });
}
const localPath = `.${req.body.src}`;
const buffer = FS.readFileSync(localPath);
const { cid } = await PowerGate.ffs.addToHot(buffer);
const { jobId } = await PowerGate.ffs.pushConfig(cid);
// TODO(jim): Refactor this so we repeat this less often.
let write = false;
for (let i = 0; i < state.library.length; i++) {
for (let j = 0; j < state.library[i].children.length; j++) {
if (localPath === state.library[i].children[j].id) {
state.library[i].children[j].job_id = jobId;
state.library[i].children[j].cid = cid;
state.library[i].children[j].storage_status = 1;
write = true;
}
}
}
// NOTE(jim): Writes the updated deal state.
if (write) {
const dirnameLibrary = path.join(__dirname, "/.data/library.json");
FS.writeFileSync(
dirnameLibrary,
JSON.stringify({ library: state.library })
);
}
state = await Utilities.emitState({ state, client, PG: PowerGate });
return res.status(200).send({ success: true, cid, jobId });
});
server.post("/_/storage/:file", async (req, res) => {
const form = formidable({
multiples: true,
uploadDir: Constants.FILE_STORAGE_URL,
});
form.parse(req, async (error, fields, files) => {
if (error) {
return res.status(500).send({ error });
} else {
// TODO(jim): Need to support other file types.
if (!files.image) {
console.error("[ prototype ] File type unspported", files);
return res
.status(500)
.send({ error: "File type unsupported", files });
}
const newPath = form.uploadDir + req.params.file;
FS.rename(files.image.path, newPath, function(err) {});
const localFile = Utilities.createFile({
id: newPath,
data: files.image,
});
// TODO(jim): Messy, refactor.
let pushed = false;
for (let i = 0; i < state.library.length; i++) {
if (!pushed) {
state.library[i].children.push(localFile);
pushed = true;
break;
}
}
// NOTE(jim): Writes the added file.
if (pushed) {
const dirnameLibrary = path.join(__dirname, "/.data/library.json");
FS.writeFileSync(
dirnameLibrary,
JSON.stringify({ library: state.library })
);
}
state = await Utilities.emitState({
state,
client,
PG: PowerGate,
});
return res.status(200).send({ success: true, file: localFile });
}
});
});
server.post("/_/upload/avatar", async (req, res) => {
const form = formidable({
multiples: true,
uploadDir: Constants.AVATAR_STORAGE_URL,
});
form.parse(req, async (error, fields, files) => {
if (error) {
return res.status(500).send({ error });
} else {
const newName = `avatar-${uuid()}.png`;
const newPath = form.uploadDir + newName;
FS.rename(files.image.path, newPath, function(err) {});
// NOTE(jim): updates avatar photo.
state.local.photo = __dirname + `/static/system/${newName}`;
const dirnameLocalSettings = path.join(
__dirname,
"/.data/local-settings.json"
);
FS.writeFileSync(
dirnameLocalSettings,
JSON.stringify({ local: { ...state.local } })
);
state = await Utilities.emitState({
state,
client,
PG: PowerGate,
});
return res.status(200).send({ success: true });
}
});
});
server.post("/_/settings", async (req, res) => {
let data;
try {
data = await PowerGate.ffs.setDefaultConfig(req.body.config);
} catch (e) {
return res.status(500).send({ error: e.message });
}
state = await Utilities.emitState({ state, client, PG: PowerGate });
return res.status(200).send({ success: true, data });
});
server.post("/_/local-settings", async (req, res) => {
state.local = { ...state.local, ...req.body.local };
const dirnameLocalSettings = path.join(
__dirname,
"/.data/local-settings.json"
);
FS.writeFileSync(
dirnameLocalSettings,
JSON.stringify({ local: { ...state.local } })
);
state = await Utilities.emitState({ state, client, PG: PowerGate });
return res.status(200).send({ success: true });
});
server.post("/_/wallet/create", async (req, res) => {
let data;
try {
data = await PowerGate.ffs.newAddr(
req.body.name,
req.body.type,
req.body.makeDefault
);
} catch (e) {
return res.status(500).send({ error: e.message });
}
state = await Utilities.emitState({ state, client, PG: PowerGate });
return res.status(200).send({ success: true, data });
});
server.post("/_/wallet/send", async (req, res) => {
let data;
try {
data = await PowerGate.ffs.sendFil(
req.body.source,
req.body.target,
req.body.amount
);
} catch (e) {
return res.status(500).send({ error: e.message });
}
state = await Utilities.emitState({ state, client, PG: PowerGate });
return res
.status(200)
.send({ success: true, data: { ...data, ...req.body } });
});
server.get("/application", async (req, res) => { server.get("/application", async (req, res) => {
return app.render(req, res, "/application", { return app.render(req, res, "/application", {
wsPort, wsPort: null,
production: productionWeb,
}); });
}); });
server.get("/", async (req, res) => { server.get("/", async (req, res) => {
return app.render(req, res, "/", { hide: productionWeb }); return app.render(req, res, "/");
}); });
server.get("*", async (req, res) => { server.all("*", async (req, res) => {
return nextRequestHandler(req, res, req.url); return handler(req, res, req.url);
}); });
server.listen(port, async (err) => { server.listen(port, async (e) => {
if (err) { if (e) throw e;
throw err;
}
console.log(`[ prototype ] client: http://localhost:${port}`); console.log(`[ slate ] client: http://localhost:${port}`);
console.log(`[ prototype ] constants:`, Constants);
console.log(
`[ prototype ] .env postgres hostname: ${process.env.POSTGRES_HOSTNAME}`
);
if (!productionWeb) {
await setIntervalViewerUpdatesUnsafe();
}
}); });
}); });